0.7.27 +Refactor
This commit is contained in:
34
kb_demo_app/frontend/ts/bindings/DemoHttpExecutionPayload.ts
Normal file
34
kb_demo_app/frontend/ts/bindings/DemoHttpExecutionPayload.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one demo HTTP execution.
|
||||
*/
|
||||
export type DemoHttpExecutionPayload = {
|
||||
/**
|
||||
* Selected endpoint name.
|
||||
*/
|
||||
endpointName: string,
|
||||
/**
|
||||
* Selected endpoint provider.
|
||||
*/
|
||||
provider: string,
|
||||
/**
|
||||
* Selected endpoint URL.
|
||||
*/
|
||||
endpointUrl: string,
|
||||
/**
|
||||
* Requested role.
|
||||
*/
|
||||
role: string,
|
||||
/**
|
||||
* Executed method name.
|
||||
*/
|
||||
method: string,
|
||||
/**
|
||||
* Classified method family.
|
||||
*/
|
||||
methodClass: string,
|
||||
/**
|
||||
* Pretty-printed JSON response payload.
|
||||
*/
|
||||
responseJson: string, };
|
||||
24
kb_demo_app/frontend/ts/bindings/DemoHttpRequest.ts
Normal file
24
kb_demo_app/frontend/ts/bindings/DemoHttpRequest.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one demo HTTP execution.
|
||||
*/
|
||||
export type DemoHttpRequest = {
|
||||
/**
|
||||
* Logical role used to select one endpoint from the pool.
|
||||
*/
|
||||
role: string,
|
||||
/**
|
||||
* JSON-RPC HTTP method name.
|
||||
*/
|
||||
method: string,
|
||||
/**
|
||||
* Optional first string argument, used by methods such as
|
||||
* `getBalance`, `getAccountInfo`, `getProgramAccounts`,
|
||||
* `getSignaturesForAddress`, `getTransaction`, `sendTransaction`.
|
||||
*/
|
||||
firstArg: string | null,
|
||||
/**
|
||||
* Optional JSON config payload encoded as a string.
|
||||
*/
|
||||
configJson: string | null, };
|
||||
@@ -0,0 +1,27 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2CatalogPayload } from "./DemoPipeline2CatalogPayload";
|
||||
|
||||
/**
|
||||
* Shared backfill response payload.
|
||||
*/
|
||||
export type DemoPipeline2BackfillPayload = {
|
||||
/**
|
||||
* Object key used by the backfill.
|
||||
*/
|
||||
objectKey: string,
|
||||
/**
|
||||
* Mode: `tokenMint` or `poolAddress`.
|
||||
*/
|
||||
mode: string,
|
||||
/**
|
||||
* HTTP role used.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Refreshed local catalog after backfill.
|
||||
*/
|
||||
catalog: DemoPipeline2CatalogPayload, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for pool backfill.
|
||||
*/
|
||||
export type DemoPipeline2BackfillPoolRequest = {
|
||||
/**
|
||||
* Pool address to backfill.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional HTTP role.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Limit for signatures fetched from the pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for token backfill.
|
||||
*/
|
||||
export type DemoPipeline2BackfillTokenRequest = {
|
||||
/**
|
||||
* Token mint to backfill.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* Optional HTTP role.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Limit for signatures fetched from the mint.
|
||||
*/
|
||||
mintSignatureLimit: number,
|
||||
/**
|
||||
* Limit for signatures fetched from each discovered pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2PairItem } from "./DemoPipeline2PairItem";
|
||||
import type { DemoPipeline2PoolItem } from "./DemoPipeline2PoolItem";
|
||||
import type { DemoPipeline2TokenItem } from "./DemoPipeline2TokenItem";
|
||||
|
||||
/**
|
||||
* Full local catalog payload.
|
||||
*/
|
||||
export type DemoPipeline2CatalogPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Observed token list.
|
||||
*/
|
||||
tokens: Array<DemoPipeline2TokenItem>,
|
||||
/**
|
||||
* Known pool list.
|
||||
*/
|
||||
pools: Array<DemoPipeline2PoolItem>,
|
||||
/**
|
||||
* Known pair list.
|
||||
*/
|
||||
pairs: Array<DemoPipeline2PairItem>, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local decoded-event diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDecodedEventDiagnosticSummary = {
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string,
|
||||
/**
|
||||
* Event category.
|
||||
*/
|
||||
eventCategory: string | null,
|
||||
/**
|
||||
* Trade candidate flag.
|
||||
*/
|
||||
tradeCandidate: boolean | null,
|
||||
/**
|
||||
* Candle candidate flag.
|
||||
*/
|
||||
candleCandidate: boolean | null,
|
||||
/**
|
||||
* Event count.
|
||||
*/
|
||||
eventCount: number,
|
||||
/**
|
||||
* Linked trade-event count.
|
||||
*/
|
||||
tradeEventCount: number, };
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local DEX diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDexDiagnosticSummary = {
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Pool count.
|
||||
*/
|
||||
poolCount: number,
|
||||
/**
|
||||
* Pair count.
|
||||
*/
|
||||
pairCount: number,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Decoded candle candidate count.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,19 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineDiagnosticSummary } from "./DemoPipeline2LocalPipelineDiagnosticSummary";
|
||||
|
||||
/**
|
||||
* Local diagnostics payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDiagnosticsPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Pretty JSON diagnostics summary.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Structured diagnostics summary.
|
||||
*/
|
||||
summary: DemoPipeline2LocalPipelineDiagnosticSummary, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local duplicate decoded-event trade diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample = {
|
||||
/**
|
||||
* Decoded event id.
|
||||
*/
|
||||
decodedEventId: number,
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string | null,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string | null,
|
||||
/**
|
||||
* Pool account.
|
||||
*/
|
||||
poolAccount: string | null,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Trade event ids.
|
||||
*/
|
||||
tradeEventIds: string | null,
|
||||
/**
|
||||
* Signatures.
|
||||
*/
|
||||
signatures: string | null, };
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local missing-trade-event diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMissingTradeEventDiagnosticSample = {
|
||||
/**
|
||||
* Decoded event id.
|
||||
*/
|
||||
decodedEventId: number,
|
||||
/**
|
||||
* Chain transaction id.
|
||||
*/
|
||||
transactionId: number | null,
|
||||
/**
|
||||
* Transaction signature.
|
||||
*/
|
||||
signature: string | null,
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string,
|
||||
/**
|
||||
* Pool account.
|
||||
*/
|
||||
poolAccount: string | null,
|
||||
/**
|
||||
* Whether the source transaction failed.
|
||||
*/
|
||||
transactionFailed: boolean,
|
||||
/**
|
||||
* Whether this missing trade event is actionable for validation.
|
||||
*/
|
||||
actionable: boolean,
|
||||
/**
|
||||
* Diagnostic reason explaining why no trade event was linked.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* Whether payload has an explicit base amount.
|
||||
*/
|
||||
hasBaseAmountPayload: boolean,
|
||||
/**
|
||||
* Whether payload has an explicit quote amount.
|
||||
*/
|
||||
hasQuoteAmountPayload: boolean,
|
||||
/**
|
||||
* Whether payload has an explicit price.
|
||||
*/
|
||||
hasPricePayload: boolean, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local missing-trade-event reason summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMissingTradeEventReasonSummary = {
|
||||
/**
|
||||
* Diagnostic reason.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* Whether grouped source transactions failed.
|
||||
*/
|
||||
transactionFailed: boolean,
|
||||
/**
|
||||
* Whether grouped missing trade events are actionable.
|
||||
*/
|
||||
actionable: boolean,
|
||||
/**
|
||||
* Total missing trade events in this group.
|
||||
*/
|
||||
eventCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit base amount payload.
|
||||
*/
|
||||
hasBaseAmountPayloadCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit quote amount payload.
|
||||
*/
|
||||
hasQuoteAmountPayloadCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit price payload.
|
||||
*/
|
||||
hasPricePayloadCount: number, };
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local multi-trade signature/pair diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample = {
|
||||
/**
|
||||
* Transaction signature.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string | null,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string | null,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Distinct decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Trade event ids.
|
||||
*/
|
||||
tradeEventIds: string | null,
|
||||
/**
|
||||
* Decoded event ids.
|
||||
*/
|
||||
decodedEventIds: string | null, };
|
||||
@@ -0,0 +1,70 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local non-actionable pair diagnostic summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalNonActionablePairDiagnosticSummary = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base token mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base token symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote token mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote token symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Total decoded trade candidates attached to the pool.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Total non-actionable missing trade events attached to the pair.
|
||||
*/
|
||||
nonActionableMissingTradeEventCount: number,
|
||||
/**
|
||||
* Total missing trade candidates caused by failed transactions.
|
||||
*/
|
||||
failedTransactionCandidateCount: number,
|
||||
/**
|
||||
* Total successful missing trade candidates without token mints.
|
||||
*/
|
||||
okTransactionWithoutTokenMintsCount: number,
|
||||
/**
|
||||
* Total successful missing trade candidates without amount payload.
|
||||
*/
|
||||
okTransactionWithoutAmountPayloadCount: number,
|
||||
/**
|
||||
* Distinct non-actionable diagnostic reasons seen for this pair.
|
||||
*/
|
||||
reasonSummary: string | null,
|
||||
/**
|
||||
* Total trade events attached to the pair.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Total candle buckets attached to the pair.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local pair diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPairDiagnosticSummary = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Decoded candle candidate count.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Invalid trade event count.
|
||||
*/
|
||||
invalidTradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number,
|
||||
/**
|
||||
* Last known price.
|
||||
*/
|
||||
lastPriceQuotePerBase: number | null, };
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local pair gap diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPairGapDiagnosticSample = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,164 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalDecodedEventDiagnosticSummary } from "./DemoPipeline2LocalDecodedEventDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalDexDiagnosticSummary } from "./DemoPipeline2LocalDexDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample } from "./DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample";
|
||||
import type { DemoPipeline2LocalMissingTradeEventDiagnosticSample } from "./DemoPipeline2LocalMissingTradeEventDiagnosticSample";
|
||||
import type { DemoPipeline2LocalMissingTradeEventReasonSummary } from "./DemoPipeline2LocalMissingTradeEventReasonSummary";
|
||||
import type { DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample } from "./DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample";
|
||||
import type { DemoPipeline2LocalNonActionablePairDiagnosticSummary } from "./DemoPipeline2LocalNonActionablePairDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPairDiagnosticSummary } from "./DemoPipeline2LocalPairDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPairGapDiagnosticSample } from "./DemoPipeline2LocalPairGapDiagnosticSample";
|
||||
|
||||
/**
|
||||
* Local pipeline diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineDiagnosticSummary = {
|
||||
/**
|
||||
* Total persisted chain transactions.
|
||||
*/
|
||||
transactionCount: number,
|
||||
/**
|
||||
* Total successful chain transactions.
|
||||
*/
|
||||
okTransactionCount: number,
|
||||
/**
|
||||
* Total failed chain transactions.
|
||||
*/
|
||||
failedTransactionCount: number,
|
||||
/**
|
||||
* Total decoded DEX events.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Total decoded DEX trade candidates.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Total decoded DEX candle candidates.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Whether the local persisted pipeline has no blocking diagnostic issue.
|
||||
*/
|
||||
diagnosticsClean: boolean,
|
||||
/**
|
||||
* Number of blocking diagnostic issues.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Total trade candidates without trade event, including ignored failed transactions.
|
||||
*/
|
||||
missingTradeEventCount: number,
|
||||
/**
|
||||
* Explicit alias for decoded trade candidates without linked trade event.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event on successful transactions.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventOnOkTransactionCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event on failed transactions.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventOnFailedTransactionCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event and without explicit base/quote payload amounts.
|
||||
* Actionable missing trade events on successful transactions.
|
||||
*/
|
||||
actionableMissingTradeEventCount: number,
|
||||
/**
|
||||
* Ignored missing trade events caused by failed transactions.
|
||||
*/
|
||||
ignoredFailedTransactionTradeCandidateCount: number, decodedTradeCandidateWithoutAmountPayloadCount: number,
|
||||
/**
|
||||
* Total persisted trade events.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Total invalid trade events.
|
||||
*/
|
||||
invalidTradeEventCount: number,
|
||||
/**
|
||||
* Total persisted pair candles.
|
||||
*/
|
||||
pairCandleCount: number,
|
||||
/**
|
||||
* Real duplicate trade rows grouped by decoded event id.
|
||||
*/
|
||||
duplicateDecodedEventTradeCount: number,
|
||||
/**
|
||||
* Multi-trade groups sharing the same signature and pair id.
|
||||
*/
|
||||
multiTradeSignaturePairCount: number,
|
||||
/**
|
||||
* Total duplicate candle buckets.
|
||||
*/
|
||||
duplicateCandleBucketCount: number,
|
||||
/**
|
||||
* Total known tokens.
|
||||
*/
|
||||
tokenCount: number,
|
||||
/**
|
||||
* Total tokens missing symbol or name.
|
||||
*/
|
||||
tokenMetadataMissingCount: number,
|
||||
/**
|
||||
* Total known pools.
|
||||
*/
|
||||
poolCount: number,
|
||||
/**
|
||||
* Total known pairs.
|
||||
*/
|
||||
pairCount: number,
|
||||
/**
|
||||
* Total pairs without trade.
|
||||
*/
|
||||
pairWithoutTradeCount: number,
|
||||
/**
|
||||
* Total pairs without candle.
|
||||
*/
|
||||
pairWithoutCandleCount: number,
|
||||
/**
|
||||
* Diagnostics grouped by DEX.
|
||||
*/
|
||||
dexSummaries: Array<DemoPipeline2LocalDexDiagnosticSummary>,
|
||||
/**
|
||||
* Diagnostics grouped by pair.
|
||||
*/
|
||||
pairSummaries: Array<DemoPipeline2LocalPairDiagnosticSummary>,
|
||||
/**
|
||||
* Diagnostics grouped by decoded event kind.
|
||||
*/
|
||||
decodedEventSummaries: Array<DemoPipeline2LocalDecodedEventDiagnosticSummary>,
|
||||
/**
|
||||
* Missing trade events grouped by diagnostic reason.
|
||||
*/
|
||||
missingTradeEventReasonSummaries: Array<DemoPipeline2LocalMissingTradeEventReasonSummary>,
|
||||
/**
|
||||
* Total pairs with only non-actionable missing trade events.
|
||||
*/
|
||||
nonActionablePairCount: number,
|
||||
/**
|
||||
* Pair summaries for non-actionable missing trade events.
|
||||
*/
|
||||
nonActionablePairSummaries: Array<DemoPipeline2LocalNonActionablePairDiagnosticSummary>,
|
||||
/**
|
||||
* Samples of decoded trade candidates without linked trade event.
|
||||
*/
|
||||
missingTradeEventSamples: Array<DemoPipeline2LocalMissingTradeEventDiagnosticSample>,
|
||||
/**
|
||||
* Samples of duplicated trade rows by decoded event id.
|
||||
*/
|
||||
duplicateDecodedEventTradeSamples: Array<DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample>,
|
||||
/**
|
||||
* Samples of multi-trade signature/pair groups.
|
||||
*/
|
||||
multiTradeSignaturePairSamples: Array<DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample>,
|
||||
/**
|
||||
* Samples of pairs without trade.
|
||||
*/
|
||||
pairWithoutTradeSamples: Array<DemoPipeline2LocalPairGapDiagnosticSample>,
|
||||
/**
|
||||
* Samples of pairs without candle.
|
||||
*/
|
||||
pairWithoutCandleSamples: Array<DemoPipeline2LocalPairGapDiagnosticSample>, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One local pipeline validation issue for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationIssue = {
|
||||
/**
|
||||
* Stable machine-readable issue code.
|
||||
*/
|
||||
code: string,
|
||||
/**
|
||||
* Human-readable issue message.
|
||||
*/
|
||||
message: string,
|
||||
/**
|
||||
* Optional subject associated with the issue.
|
||||
*/
|
||||
subject: string | null,
|
||||
/**
|
||||
* Whether the issue blocks the validation report.
|
||||
*/
|
||||
blocking: boolean, };
|
||||
@@ -0,0 +1,35 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineValidationIssue } from "./DemoPipeline2LocalPipelineValidationIssue";
|
||||
|
||||
/**
|
||||
* Local pipeline validation report for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationReport = {
|
||||
/**
|
||||
* Stable validation profile code used to produce the report.
|
||||
*/
|
||||
validationProfileCode: string,
|
||||
/**
|
||||
* Whether the validation passed without blocking issues.
|
||||
*/
|
||||
validationPassed: boolean,
|
||||
/**
|
||||
* Number of blocking issues found by validation.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Number of non-blocking warnings found by validation.
|
||||
*/
|
||||
warningCount: number,
|
||||
/**
|
||||
* Expected DEX codes used by the validation run.
|
||||
*/
|
||||
expectedDexCodes: Array<string>,
|
||||
/**
|
||||
* Observed DEX codes found in diagnostics.
|
||||
*/
|
||||
observedDexCodes: Array<string>,
|
||||
/**
|
||||
* Issues produced by validation.
|
||||
*/
|
||||
issues: Array<DemoPipeline2LocalPipelineValidationIssue>, };
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineDiagnosticSummary } from "./DemoPipeline2LocalPipelineDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPipelineValidationReport } from "./DemoPipeline2LocalPipelineValidationReport";
|
||||
|
||||
/**
|
||||
* Local pipeline validation run for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationRun = {
|
||||
/**
|
||||
* Stable validation profile code used to produce the run.
|
||||
*/
|
||||
validationProfileCode: string,
|
||||
/**
|
||||
* Whether the validation passed without blocking issues.
|
||||
*/
|
||||
validationPassed: boolean,
|
||||
/**
|
||||
* Number of blocking issues found by validation.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Number of non-blocking warnings found by validation.
|
||||
*/
|
||||
warningCount: number,
|
||||
/**
|
||||
* Diagnostics summary that was validated.
|
||||
*/
|
||||
summary: DemoPipeline2LocalPipelineDiagnosticSummary,
|
||||
/**
|
||||
* Detailed validation report produced from the diagnostics summary.
|
||||
*/
|
||||
report: DemoPipeline2LocalPipelineValidationReport, };
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineValidationRun } from "./DemoPipeline2LocalPipelineValidationRun";
|
||||
|
||||
/**
|
||||
* Local validation payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalValidationPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Pretty JSON diagnostics summary used by validation.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Pretty JSON validation run.
|
||||
*/
|
||||
validationJson: string,
|
||||
/**
|
||||
* Structured validation run.
|
||||
*/
|
||||
run: DemoPipeline2LocalPipelineValidationRun, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Candle payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2PairCandlesPayload = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Timeframe in seconds.
|
||||
*/
|
||||
timeframeSeconds: number,
|
||||
/**
|
||||
* Pretty JSON array of candles.
|
||||
*/
|
||||
candlesJson: string, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for pair candles.
|
||||
*/
|
||||
export type DemoPipeline2PairCandlesRequest = {
|
||||
/**
|
||||
* Pair id to load.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Timeframe in seconds.
|
||||
*/
|
||||
timeframeSeconds: number,
|
||||
/**
|
||||
* Whether materialized candles should be preferred when available.
|
||||
*/
|
||||
preferMaterialized: boolean, };
|
||||
30
kb_demo_app/frontend/ts/bindings/DemoPipeline2PairItem.ts
Normal file
30
kb_demo_app/frontend/ts/bindings/DemoPipeline2PairItem.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One pair item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2PairItem = {
|
||||
/**
|
||||
* Internal pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Related pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional pair symbol.
|
||||
*/
|
||||
symbol: string | null,
|
||||
/**
|
||||
* Optional DEX code.
|
||||
*/
|
||||
dexCode: string | null,
|
||||
/**
|
||||
* Optional local trade count.
|
||||
*/
|
||||
tradeCount: number | null,
|
||||
/**
|
||||
* Optional local last price.
|
||||
*/
|
||||
lastPriceQuotePerBase: number | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2PoolItem.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2PoolItem.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One pool item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2PoolItem = {
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional internal pair id when known.
|
||||
*/
|
||||
pairId: number | null,
|
||||
/**
|
||||
* Optional DEX code.
|
||||
*/
|
||||
dexCode: string | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2TokenItem.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2TokenItem.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One token item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2TokenItem = {
|
||||
/**
|
||||
* Token mint.
|
||||
*/
|
||||
mint: string,
|
||||
/**
|
||||
* Optional token symbol.
|
||||
*/
|
||||
symbol: string | null,
|
||||
/**
|
||||
* Optional token name.
|
||||
*/
|
||||
name: string | null, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one pool backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillPoolPayload = {
|
||||
/**
|
||||
* Backfilled pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* HTTP role used during backfill.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary returned by `TokenBackfillService::backfill_pool_by_address`.
|
||||
*/
|
||||
backfillJson: string,
|
||||
/**
|
||||
* Whether the pool exists in persisted pool objects after backfill.
|
||||
*/
|
||||
poolPersistedAfterBackfill: boolean, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pool backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillPoolRequest = {
|
||||
/**
|
||||
* Pool address to backfill.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* HTTP role used to select one endpoint in the pool.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Maximum number of signatures fetched from the pool address.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one token backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillTokenPayload = {
|
||||
/**
|
||||
* Backfilled token mint.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* HTTP role used during backfill.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary returned by `TokenBackfillService`.
|
||||
*/
|
||||
backfillJson: string,
|
||||
/**
|
||||
* Whether the token exists in persisted token objects after backfill.
|
||||
*/
|
||||
tokenPersistedAfterBackfill: boolean, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one token backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillTokenRequest = {
|
||||
/**
|
||||
* Token mint to backfill.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* HTTP role used to select one endpoint in the pool.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Maximum number of signatures fetched directly from the mint.
|
||||
*/
|
||||
mintSignatureLimit: number,
|
||||
/**
|
||||
* Maximum number of signatures fetched from each discovered pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by pair id.
|
||||
*/
|
||||
export type DemoPipelineInspectPairRequest = {
|
||||
/**
|
||||
* Pair id to inspect.
|
||||
*/
|
||||
pairId: bigint,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,58 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one pipeline inspection.
|
||||
*/
|
||||
export type DemoPipelineInspectPayload = {
|
||||
/**
|
||||
* Inspected signature.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Summary JSON block.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Resolved transaction JSON block.
|
||||
*/
|
||||
transactionJson: string,
|
||||
/**
|
||||
* Decoded events JSON block.
|
||||
*/
|
||||
decodedEventsJson: string,
|
||||
/**
|
||||
* Pools JSON block.
|
||||
*/
|
||||
poolsJson: string,
|
||||
/**
|
||||
* Pairs JSON block.
|
||||
*/
|
||||
pairsJson: string,
|
||||
/**
|
||||
* Launch attributions JSON block.
|
||||
*/
|
||||
launchAttributionsJson: string,
|
||||
/**
|
||||
* Pool origins JSON block.
|
||||
*/
|
||||
poolOriginsJson: string,
|
||||
/**
|
||||
* Wallet inspection JSON block.
|
||||
*/
|
||||
walletsJson: string,
|
||||
/**
|
||||
* Trade events JSON block.
|
||||
*/
|
||||
tradeEventsJson: string,
|
||||
/**
|
||||
* Pair metrics JSON block.
|
||||
*/
|
||||
pairMetricsJson: string,
|
||||
/**
|
||||
* Pair candles JSON block.
|
||||
*/
|
||||
pairCandlesJson: string,
|
||||
/**
|
||||
* Pair analytic signals JSON block.
|
||||
*/
|
||||
pairAnalyticSignalsJson: string, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by pool address.
|
||||
*/
|
||||
export type DemoPipelineInspectPoolRequest = {
|
||||
/**
|
||||
* Pool address to inspect.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by signature.
|
||||
*/
|
||||
export type DemoPipelineInspectRequest = {
|
||||
/**
|
||||
* Transaction signature to inspect.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by token mint.
|
||||
*/
|
||||
export type DemoPipelineInspectTokenRequest = {
|
||||
/**
|
||||
* Token mint to inspect.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Endpoint summary sent to the demo frontend.
|
||||
*/
|
||||
export type DemoWsEndpointSummary = { name: string, resolvedUrl: string, provider: string, enabled: boolean, roles: Array<string>, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Static endpoint summary enriched with current manager state.
|
||||
*/
|
||||
export type DemoWsManagerEndpointSummary = { name: string, resolvedUrl: string, provider: string, roles: Array<string>, connectionState: string, activeSubscriptionCount: number, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoWsManagerEndpointSummary } from "./DemoWsManagerEndpointSummary";
|
||||
|
||||
/**
|
||||
* Global demo manager snapshot payload.
|
||||
*/
|
||||
export type DemoWsManagerSnapshotPayload = { endpointCount: number, startedCount: number, endpoints: Array<DemoWsManagerEndpointSummary>, };
|
||||
6
kb_demo_app/frontend/ts/bindings/DemoWsStatusPayload.ts
Normal file
6
kb_demo_app/frontend/ts/bindings/DemoWsStatusPayload.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Current demo window runtime status.
|
||||
*/
|
||||
export type DemoWsStatusPayload = { connectionState: string, endpointName: string | null, endpointUrl: string | null, currentSubscriptionId: bigint | null, currentSubscribeMethod: string | null, currentUnsubscribeMethod: string | null, currentNotificationMethod: string | null, eventCountTotal: bigint, notificationCountTotal: bigint, uiLogCount: bigint, suppressedLogCount: bigint, lastEventKind: string | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Subscribe request sent by the demo frontend.
|
||||
*/
|
||||
export type DemoWsSubscribeRequest = { method: string, mode: string, target: string | null, filterJson: string | null, configJson: string | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/SplashOrder.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/SplashOrder.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Command payload sent from Rust to the splash frontend.
|
||||
*/
|
||||
export type SplashOrder = {
|
||||
/**
|
||||
* Splash command name such as `fadein`, `fadeout`, `add_msg`, or `add_log`.
|
||||
*/
|
||||
order: string,
|
||||
/**
|
||||
* Optional message payload attached to the command.
|
||||
*/
|
||||
msg: string | null,
|
||||
/**
|
||||
* Optional status payload attached to the command.
|
||||
*/
|
||||
status: string | null, };
|
||||
422
kb_demo_app/frontend/ts/demo_http.ts
Normal file
422
kb_demo_app/frontend/ts/demo_http.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoHttpRequest } from './bindings/DemoHttpRequest.ts';
|
||||
import { DemoHttpExecutionPayload } from './bindings/DemoHttpExecutionPayload.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
interface DemoHttpPoolClientSnapshot {
|
||||
endpointName: string;
|
||||
provider: string;
|
||||
endpointUrl: string;
|
||||
roles: string[];
|
||||
status: string;
|
||||
pausedRemainingMs: number | null;
|
||||
availableConcurrencySlots: number;
|
||||
}
|
||||
|
||||
let demoHttpLastResponseRawText = "";
|
||||
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
|
||||
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
lines.push(`[${timestamp}] ${line}`);
|
||||
|
||||
const maxLines = 400;
|
||||
textarea.value = lines.slice(-maxLines).join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function methodNeedsFirstArg(method: string): boolean {
|
||||
return [
|
||||
"getBalance",
|
||||
"getAccountInfo",
|
||||
"getProgramAccounts",
|
||||
"getSignaturesForAddress",
|
||||
"getTransaction",
|
||||
"sendTransaction",
|
||||
].includes(method);
|
||||
}
|
||||
|
||||
function firstArgLabelForMethod(method: string): string {
|
||||
if (method === "getBalance") return "Address";
|
||||
if (method === "getAccountInfo") return "Address";
|
||||
if (method === "getProgramAccounts") return "Program id";
|
||||
if (method === "getSignaturesForAddress") return "Address";
|
||||
if (method === "getTransaction") return "Signature";
|
||||
if (method === "sendTransaction") return "Encoded transaction (base64)";
|
||||
return "First arg";
|
||||
}
|
||||
|
||||
function firstArgHelpForMethod(method: string): string {
|
||||
if (method === "getBalance") return "Adresse Solana dont on veut lire le solde.";
|
||||
if (method === "getAccountInfo") return "Adresse Solana du compte à lire.";
|
||||
if (method === "getProgramAccounts") return "Program id dont on veut lister les comptes.";
|
||||
if (method === "getSignaturesForAddress") return "Adresse Solana dont on veut lire les signatures récentes.";
|
||||
if (method === "getTransaction") return "Signature exacte d’une transaction.";
|
||||
if (method === "sendTransaction") {
|
||||
return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion d’erreur.";
|
||||
}
|
||||
return "Adresse, program id, signature ou transaction encodée selon la méthode.";
|
||||
}
|
||||
|
||||
function configHelpForMethod(method: string): string {
|
||||
if (method === "getProgramAccounts") {
|
||||
return "Preset volontairement filtré pour éviter une réponse trop large.";
|
||||
}
|
||||
|
||||
if (method === "sendTransaction") {
|
||||
return "Configuration d’envoi. Avec le preset de transaction invalide, la requête doit échouer côté RPC.";
|
||||
}
|
||||
|
||||
return "Configuration JSON optionnelle.";
|
||||
}
|
||||
|
||||
function defaultRoleForMethod(method: string): string {
|
||||
if (method === "sendTransaction") return "http_transactions";
|
||||
if (method === "getProgramAccounts") return "http_heavy";
|
||||
return "http_queries";
|
||||
}
|
||||
|
||||
function defaultFirstArgForMethod(method: string): string {
|
||||
if (method === "getBalance") return "11111111111111111111111111111111";
|
||||
if (method === "getAccountInfo") return "11111111111111111111111111111111";
|
||||
if (method === "getProgramAccounts") return "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
||||
if (method === "getSignaturesForAddress") return "11111111111111111111111111111111";
|
||||
if (method === "getTransaction") {
|
||||
return "5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7";
|
||||
}
|
||||
if (method === "sendTransaction") return "AQ==";
|
||||
return "";
|
||||
}
|
||||
|
||||
function defaultConfigForMethod(method: string): string {
|
||||
if (method === "getSlot" || method === "getBlockHeight" || method === "getLatestBlockhash") {
|
||||
return `{"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getBalance") {
|
||||
return `{"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getAccountInfo") {
|
||||
return `{"encoding":"base64","commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getProgramAccounts") {
|
||||
return `{
|
||||
"encoding":"base64",
|
||||
"commitment":"confirmed",
|
||||
"dataSlice":{"offset":0,"length":0},
|
||||
"filters":[
|
||||
{"dataSize":165},
|
||||
{"memcmp":{"offset":32,"bytes":"11111111111111111111111111111111"}}
|
||||
]
|
||||
}`;
|
||||
}
|
||||
|
||||
if (method === "getSignaturesForAddress") {
|
||||
return `{"limit":10,"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getTransaction") {
|
||||
return `{"encoding":"json","commitment":"confirmed","maxSupportedTransactionVersion":0}`;
|
||||
}
|
||||
|
||||
if (method === "sendTransaction") {
|
||||
return `{"encoding":"base64","skipPreflight":true,"maxRetries":0}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderPoolSnapshots(
|
||||
snapshots: DemoHttpPoolClientSnapshot[],
|
||||
tableBody: HTMLTableSectionElement,
|
||||
): void {
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const endpointCell = document.createElement("td");
|
||||
endpointCell.innerHTML = `<div class="fw-semibold">${snapshot.endpointName}</div><div class="small text-body-secondary">${snapshot.endpointUrl}</div>`;
|
||||
|
||||
const providerCell = document.createElement("td");
|
||||
providerCell.textContent = snapshot.provider;
|
||||
|
||||
const statusCell = document.createElement("td");
|
||||
statusCell.textContent = snapshot.status;
|
||||
|
||||
const pausedCell = document.createElement("td");
|
||||
pausedCell.textContent = snapshot.pausedRemainingMs === null ? "-" : String(snapshot.pausedRemainingMs);
|
||||
|
||||
const concurrencyCell = document.createElement("td");
|
||||
concurrencyCell.textContent = String(snapshot.availableConcurrencySlots);
|
||||
|
||||
const rolesCell = document.createElement("td");
|
||||
rolesCell.textContent = snapshot.roles.join(", ");
|
||||
|
||||
row.appendChild(endpointCell);
|
||||
row.appendChild(providerCell);
|
||||
row.appendChild(statusCell);
|
||||
row.appendChild(pausedCell);
|
||||
row.appendChild(concurrencyCell);
|
||||
row.appendChild(rolesCell);
|
||||
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderResponse(
|
||||
responseTextarea: HTMLTextAreaElement,
|
||||
prettyToggle: HTMLInputElement,
|
||||
): void {
|
||||
if (demoHttpLastResponseRawText.trim() === "") {
|
||||
responseTextarea.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prettyToggle.checked) {
|
||||
responseTextarea.value = demoHttpLastResponseRawText;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(demoHttpLastResponseRawText);
|
||||
responseTextarea.value = JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
responseTextarea.value = demoHttpLastResponseRawText;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string): Promise<void> {
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempTextarea = document.createElement("textarea");
|
||||
tempTextarea.value = text;
|
||||
tempTextarea.style.position = "fixed";
|
||||
tempTextarea.style.left = "-9999px";
|
||||
document.body.appendChild(tempTextarea);
|
||||
tempTextarea.focus();
|
||||
tempTextarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(tempTextarea);
|
||||
}
|
||||
|
||||
function updateMethodForm(
|
||||
roleSelect: HTMLSelectElement,
|
||||
methodSelect: HTMLSelectElement,
|
||||
firstArgGroup: HTMLDivElement,
|
||||
firstArgLabel: HTMLLabelElement,
|
||||
firstArgHelp: HTMLDivElement,
|
||||
firstArgInput: HTMLInputElement,
|
||||
configTextarea: HTMLTextAreaElement,
|
||||
configHelp: HTMLDivElement,
|
||||
): void {
|
||||
const method = methodSelect.value;
|
||||
|
||||
firstArgGroup.style.display = methodNeedsFirstArg(method) ? "" : "none";
|
||||
firstArgLabel.textContent = firstArgLabelForMethod(method);
|
||||
firstArgHelp.textContent = firstArgHelpForMethod(method);
|
||||
firstArgInput.value = defaultFirstArgForMethod(method);
|
||||
configTextarea.value = defaultConfigForMethod(method);
|
||||
configHelp.textContent = configHelpForMethod(method);
|
||||
roleSelect.value = defaultRoleForMethod(method);
|
||||
}
|
||||
|
||||
async function refreshPoolSnapshot(
|
||||
tableBody: HTMLTableSectionElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
shouldLog: boolean,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const snapshots = await invoke<DemoHttpPoolClientSnapshot[]>("demo_http_list_pool_clients");
|
||||
renderPoolSnapshots(snapshots, tableBody);
|
||||
|
||||
if (shouldLog) {
|
||||
appendLogLine(logTextarea, `[ui] refreshed http pool snapshot (${snapshots.length} endpoint(s))`);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] refresh pool error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_http window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const roleSelect = document.querySelector<HTMLSelectElement>("#demoHttpRoleSelect");
|
||||
const methodSelect = document.querySelector<HTMLSelectElement>("#demoHttpMethodSelect");
|
||||
const firstArgGroup = document.querySelector<HTMLDivElement>("#demoHttpFirstArgGroup");
|
||||
const firstArgLabel = document.querySelector<HTMLLabelElement>("#demoHttpFirstArgLabel");
|
||||
const firstArgHelp = document.querySelector<HTMLDivElement>("#demoHttpFirstArgHelp");
|
||||
const firstArgInput = document.querySelector<HTMLInputElement>("#demoHttpFirstArgInput");
|
||||
const configTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpConfigTextarea");
|
||||
const configHelp = document.querySelector<HTMLDivElement>("#demoHttpConfigHelp");
|
||||
const executeButton = document.querySelector<HTMLButtonElement>("#demoHttpExecuteButton");
|
||||
const refreshPoolButton = document.querySelector<HTMLButtonElement>("#demoHttpRefreshPoolButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoHttpClearLogButton");
|
||||
const copyResponseButton = document.querySelector<HTMLButtonElement>("#demoHttpCopyResponseButton");
|
||||
const prettyToggle = document.querySelector<HTMLInputElement>("#demoHttpPrettyToggle");
|
||||
const responseTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpResponseTextarea");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpLogTextarea");
|
||||
const poolTableBody = document.querySelector<HTMLTableSectionElement>("#demoHttpPoolTableBody");
|
||||
const lastEndpointText = document.querySelector<HTMLSpanElement>("#demoHttpLastEndpointText");
|
||||
const lastProviderText = document.querySelector<HTMLSpanElement>("#demoHttpLastProviderText");
|
||||
const lastMethodClassText = document.querySelector<HTMLSpanElement>("#demoHttpLastMethodClassText");
|
||||
|
||||
if (
|
||||
!roleSelect ||
|
||||
!methodSelect ||
|
||||
!firstArgGroup ||
|
||||
!firstArgLabel ||
|
||||
!firstArgHelp ||
|
||||
!firstArgInput ||
|
||||
!configTextarea ||
|
||||
!configHelp ||
|
||||
!executeButton ||
|
||||
!refreshPoolButton ||
|
||||
!clearLogButton ||
|
||||
!copyResponseButton ||
|
||||
!prettyToggle ||
|
||||
!responseTextarea ||
|
||||
!logTextarea ||
|
||||
!poolTableBody ||
|
||||
!lastEndpointText ||
|
||||
!lastProviderText ||
|
||||
!lastMethodClassText
|
||||
) {
|
||||
debug("demo_http UI controls not found");
|
||||
return;
|
||||
}
|
||||
|
||||
updateMethodForm(
|
||||
roleSelect,
|
||||
methodSelect,
|
||||
firstArgGroup,
|
||||
firstArgLabel,
|
||||
firstArgHelp,
|
||||
firstArgInput,
|
||||
configTextarea,
|
||||
configHelp,
|
||||
);
|
||||
|
||||
methodSelect.addEventListener("change", () => {
|
||||
updateMethodForm(
|
||||
roleSelect,
|
||||
methodSelect,
|
||||
firstArgGroup,
|
||||
firstArgLabel,
|
||||
firstArgHelp,
|
||||
firstArgInput,
|
||||
configTextarea,
|
||||
configHelp,
|
||||
);
|
||||
});
|
||||
|
||||
prettyToggle.addEventListener("change", () => {
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
});
|
||||
|
||||
refreshPoolButton.addEventListener("click", async () => {
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
copyResponseButton.addEventListener("click", async () => {
|
||||
if (responseTextarea.value.trim() === "") {
|
||||
appendLogLine(logTextarea, "[ui] copy skipped: no response available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyTextToClipboard(responseTextarea.value);
|
||||
appendLogLine(logTextarea, "[ui] response copied to clipboard");
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] copy response error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
executeButton.addEventListener("click", async () => {
|
||||
const request: DemoHttpRequest = {
|
||||
role: roleSelect.value,
|
||||
method: methodSelect.value,
|
||||
firstArg: firstArgInput.value.trim() === "" ? null : firstArgInput.value.trim(),
|
||||
configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await invoke<DemoHttpExecutionPayload>("demo_http_execute_request", { request });
|
||||
|
||||
lastEndpointText.textContent = `${response.endpointName} (${response.endpointUrl})`;
|
||||
lastProviderText.textContent = response.provider;
|
||||
lastMethodClassText.textContent = response.methodClass;
|
||||
|
||||
demoHttpLastResponseRawText = response.responseJson;
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] ${response.method} via ${response.endpointName} / role=${response.role} / class=${response.methodClass}`,
|
||||
);
|
||||
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
} catch (error) {
|
||||
demoHttpLastResponseRawText = String(error);
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] execute error: ${String(error)}`);
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
}
|
||||
});
|
||||
|
||||
appendLogLine(logTextarea, "[ui] demo_http window loaded");
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
});
|
||||
1087
kb_demo_app/frontend/ts/demo_pipeline.ts
Normal file
1087
kb_demo_app/frontend/ts/demo_pipeline.ts
Normal file
File diff suppressed because it is too large
Load Diff
712
kb_demo_app/frontend/ts/demo_pipeline2.ts
Normal file
712
kb_demo_app/frontend/ts/demo_pipeline2.ts
Normal file
@@ -0,0 +1,712 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_pipeline2.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import * as echarts from "echarts";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
|
||||
import type { DemoPipeline2CatalogPayload } from "./bindings/DemoPipeline2CatalogPayload.ts";
|
||||
import type { DemoPipeline2BackfillTokenRequest } from "./bindings/DemoPipeline2BackfillTokenRequest.ts";
|
||||
import type { DemoPipeline2BackfillPoolRequest } from "./bindings/DemoPipeline2BackfillPoolRequest.ts";
|
||||
import type { DemoPipeline2BackfillPayload } from "./bindings/DemoPipeline2BackfillPayload.ts";
|
||||
import type { DemoPipeline2PairCandlesRequest } from "./bindings/DemoPipeline2PairCandlesRequest.ts";
|
||||
import type { DemoPipeline2PairCandlesPayload } from "./bindings/DemoPipeline2PairCandlesPayload.ts";
|
||||
import type { DemoPipeline2LocalDiagnosticsPayload } from "./bindings/DemoPipeline2LocalDiagnosticsPayload.ts";
|
||||
import type { DemoPipeline2LocalValidationPayload } from "./bindings/DemoPipeline2LocalValidationPayload.ts";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
interface PairCandle {
|
||||
id: number | null;
|
||||
pair_id: number;
|
||||
timeframe_seconds: number;
|
||||
bucket_start_unix: number;
|
||||
bucket_end_unix: number;
|
||||
open_price_quote_per_base: number;
|
||||
high_price_quote_per_base: number;
|
||||
low_price_quote_per_base: number;
|
||||
close_price_quote_per_base: number;
|
||||
trade_count: number;
|
||||
buy_count: number;
|
||||
sell_count: number;
|
||||
base_volume_raw: string | null;
|
||||
quote_volume_raw: string | null;
|
||||
first_trade_signature: string | null;
|
||||
last_trade_signature: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
|
||||
interface LocalPipelineReplayResult {
|
||||
selectedTransactionCount: number;
|
||||
replayedTransactionCount: number;
|
||||
decodeErrorCount: number;
|
||||
detectErrorCount: number;
|
||||
tradeAggregationErrorCount: number;
|
||||
pairCandleErrorCount: number;
|
||||
analyticSignalErrorCount: number;
|
||||
decodedEventCount: number;
|
||||
detectionCount: number;
|
||||
tradeEventCount: number;
|
||||
pairCandleUpsertCount: number;
|
||||
analyticSignalUpsertCount: number;
|
||||
tokenMetadataUpdatedCount: number;
|
||||
pairSymbolUpdatedCount: number;
|
||||
globalErrorCount: number;
|
||||
}
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
|
||||
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
lines.push(`[${timestamp}] ${line}`);
|
||||
|
||||
textarea.value = lines.slice(-300).join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function setEmptyChart(
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
message: string,
|
||||
): void {
|
||||
chartMeta.textContent = message;
|
||||
|
||||
chart.setOption({
|
||||
animation: false,
|
||||
title: {
|
||||
text: message,
|
||||
left: "center",
|
||||
top: "middle",
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "normal",
|
||||
},
|
||||
},
|
||||
tooltip: {},
|
||||
xAxis: { show: false, type: "category", data: [] },
|
||||
yAxis: { show: false, type: "value" },
|
||||
series: [],
|
||||
}, true);
|
||||
}
|
||||
|
||||
function readPositiveIntegerInput(
|
||||
input: HTMLInputElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
label: string,
|
||||
): number | undefined {
|
||||
const text = input.value.trim();
|
||||
if (text === "") {
|
||||
appendLogLine(logTextarea, `[ui] ${label} is required`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(text, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
function readOptionalPositiveIntegerInput(
|
||||
input: HTMLInputElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
label: string,
|
||||
): number | null | undefined {
|
||||
const text = input.value.trim();
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(text, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
function refreshPairSelect(
|
||||
catalog: DemoPipeline2CatalogPayload,
|
||||
select: HTMLSelectElement,
|
||||
): void {
|
||||
const previousValue = select.value;
|
||||
|
||||
select.innerHTML = "";
|
||||
const emptyOption = document.createElement("option");
|
||||
emptyOption.value = "";
|
||||
emptyOption.textContent = "Aucune";
|
||||
select.appendChild(emptyOption);
|
||||
|
||||
for (const pair of catalog.pairs) {
|
||||
const option = document.createElement("option");
|
||||
option.value = pair.pairId.toString();
|
||||
option.textContent = `#${pair.pairId.toString()} ${pair.symbol ?? ""} ${pair.poolAddress}`.trim();
|
||||
if (option.value === previousValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCatalogTextareas(
|
||||
catalog: DemoPipeline2CatalogPayload,
|
||||
tokensTextarea: HTMLTextAreaElement,
|
||||
poolsTextarea: HTMLTextAreaElement,
|
||||
pairsTextarea: HTMLTextAreaElement,
|
||||
): void {
|
||||
tokensTextarea.value = JSON.stringify(catalog.tokens, null, 2);
|
||||
poolsTextarea.value = JSON.stringify(catalog.pools, null, 2);
|
||||
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
|
||||
}
|
||||
|
||||
function parseCandlesJson(raw: string): PairCandle[] {
|
||||
if (raw.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as PairCandle[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseVolume(text: string | null, fallback: number): number {
|
||||
if (text === null || text.trim() === "") {
|
||||
return Number(fallback);
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(text);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return Number(fallback);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderCandlesChart(
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
pairId: number,
|
||||
timeframeSeconds: number,
|
||||
candles: PairCandle[],
|
||||
): void {
|
||||
if (candles.length === 0) {
|
||||
setEmptyChart(chart, chartMeta, "Aucune candle disponible.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...candles].sort(
|
||||
(left, right) => left.bucket_start_unix - right.bucket_start_unix,
|
||||
);
|
||||
|
||||
const categoryData = sorted.map((candle) =>
|
||||
new Date(Number(candle.bucket_start_unix) * 1000).toLocaleString("fr-CH", {
|
||||
hour12: false,
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
);
|
||||
|
||||
const ohlcData = sorted.map((candle) => [
|
||||
candle.open_price_quote_per_base,
|
||||
candle.close_price_quote_per_base,
|
||||
candle.low_price_quote_per_base,
|
||||
candle.high_price_quote_per_base,
|
||||
]);
|
||||
|
||||
const volumeData = sorted.map((candle) =>
|
||||
parseVolume(candle.quote_volume_raw, candle.trade_count),
|
||||
);
|
||||
|
||||
chartMeta.textContent =
|
||||
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
|
||||
|
||||
chart.setOption({
|
||||
animation: false,
|
||||
legend: {
|
||||
data: ["OHLC", "Volume"],
|
||||
top: 0,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
},
|
||||
},
|
||||
axisPointer: {
|
||||
link: [{ xAxisIndex: "all" }],
|
||||
},
|
||||
grid: [
|
||||
{ left: 60, right: 24, top: 40, height: "58%" },
|
||||
{ left: 60, right: 24, top: "74%", height: "16%" },
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
gridIndex: 1,
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitArea: { show: false },
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
scale: true,
|
||||
splitNumber: 2,
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
xAxisIndex: [0, 1],
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
type: "slider",
|
||||
xAxisIndex: [0, 1],
|
||||
bottom: 6,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "OHLC",
|
||||
type: "candlestick",
|
||||
data: ohlcData,
|
||||
itemStyle: {
|
||||
color: "#16a34a",
|
||||
color0: "#dc2626",
|
||||
borderColor: "#15803d",
|
||||
borderColor0: "#b91c1c",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Volume",
|
||||
type: "bar",
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumeData,
|
||||
},
|
||||
],
|
||||
}, true);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_pipeline2 window loaded");
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
|
||||
const refreshCatalogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2RefreshCatalogButton");
|
||||
const tokensTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2TokensTextarea");
|
||||
const poolsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PoolsTextarea");
|
||||
const pairsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PairsTextarea");
|
||||
|
||||
const httpRoleInput = document.querySelector<HTMLInputElement>("#demoPipeline2HttpRoleInput");
|
||||
const mintInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintInput");
|
||||
const mintSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintSignatureLimitInput");
|
||||
const mintPoolLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintPoolLimitInput");
|
||||
const backfillMintButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillMintButton");
|
||||
|
||||
const poolInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolInput");
|
||||
const poolSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolSignatureLimitInput");
|
||||
const backfillPoolButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillPoolButton");
|
||||
|
||||
const replayLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayLimitInput");
|
||||
const replayMetadataCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataCheckbox");
|
||||
const replayMetadataLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataLimitInput");
|
||||
const replayLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ReplayLocalPipelineButton");
|
||||
const diagnoseLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2DiagnoseLocalPipelineButton");
|
||||
const validateLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ValidateLocalPipelineButton");
|
||||
|
||||
const pairSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2PairSelect");
|
||||
const timeframeSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2TimeframeSelect");
|
||||
const customTimeframeInput = document.querySelector<HTMLInputElement>("#demoPipeline2CustomTimeframeInput");
|
||||
const preferMaterializedInput = document.querySelector<HTMLInputElement>("#demoPipeline2PreferMaterializedInput");
|
||||
const loadCandlesButton = document.querySelector<HTMLButtonElement>("#demoPipeline2LoadCandlesButton");
|
||||
|
||||
const backfillSummaryTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2BackfillSummaryTextarea");
|
||||
const chartElement = document.querySelector<HTMLDivElement>("#demoPipeline2Chart");
|
||||
const chartMeta = document.querySelector<HTMLDivElement>("#demoPipeline2ChartMeta");
|
||||
const localDiagnosticsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalDiagnosticsTextarea");
|
||||
const localValidationTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalValidationTextarea");
|
||||
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ClearLogButton");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LogTextarea");
|
||||
|
||||
if (
|
||||
!refreshCatalogButton ||
|
||||
!tokensTextarea ||
|
||||
!poolsTextarea ||
|
||||
!pairsTextarea ||
|
||||
!httpRoleInput ||
|
||||
!mintInput ||
|
||||
!mintSignatureLimitInput ||
|
||||
!mintPoolLimitInput ||
|
||||
!backfillMintButton ||
|
||||
!poolInput ||
|
||||
!poolSignatureLimitInput ||
|
||||
!backfillPoolButton ||
|
||||
!replayLimitInput ||
|
||||
!replayMetadataCheckbox ||
|
||||
!replayMetadataLimitInput ||
|
||||
!replayLocalPipelineButton ||
|
||||
!diagnoseLocalPipelineButton ||
|
||||
!validateLocalPipelineButton ||
|
||||
!pairSelect ||
|
||||
!timeframeSelect ||
|
||||
!customTimeframeInput ||
|
||||
!preferMaterializedInput ||
|
||||
!loadCandlesButton ||
|
||||
!backfillSummaryTextarea ||
|
||||
!localDiagnosticsTextarea ||
|
||||
!localValidationTextarea ||
|
||||
!chartElement ||
|
||||
!chartMeta ||
|
||||
!clearLogButton ||
|
||||
!logTextarea
|
||||
) {
|
||||
console.error("demo_pipeline2 DOM is incomplete");
|
||||
return;
|
||||
}
|
||||
|
||||
const safeTokensTextarea = tokensTextarea;
|
||||
const safePoolsTextarea = poolsTextarea;
|
||||
const safePairsTextarea = pairsTextarea;
|
||||
|
||||
|
||||
const safePairSelect = pairSelect;
|
||||
const safeChartElement = chartElement;
|
||||
const safeChartMeta = chartMeta;
|
||||
|
||||
const safeLogTextarea = logTextarea;
|
||||
const safeLocalDiagnosticsTextarea = localDiagnosticsTextarea;
|
||||
|
||||
const chart = echarts.init(safeChartElement);
|
||||
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
||||
window.addEventListener("resize", () => chart.resize());
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
let currentCatalog: DemoPipeline2CatalogPayload | null = null;
|
||||
|
||||
async function refreshCatalog(): Promise<void> {
|
||||
appendLogLine(safeLogTextarea, "[ui] refreshing local catalog");
|
||||
|
||||
try {
|
||||
const catalog = await invoke<DemoPipeline2CatalogPayload>("demo_pipeline2_get_catalog");
|
||||
currentCatalog = catalog;
|
||||
|
||||
renderCatalogTextareas(catalog, safeTokensTextarea, safePoolsTextarea, safePairsTextarea);
|
||||
refreshPairSelect(catalog, safePairSelect);
|
||||
|
||||
appendLogLine(
|
||||
safeLogTextarea,
|
||||
`[ui] catalog refreshed: ${catalog.tokens.length} tokens, ${catalog.pools.length} pools, ${catalog.pairs.length} pairs`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(safeLogTextarea, `[ui] catalog refresh error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
refreshCatalogButton.addEventListener("click", () => {
|
||||
void refreshCatalog();
|
||||
});
|
||||
|
||||
backfillMintButton.addEventListener("click", async () => {
|
||||
const tokenMint = mintInput.value.trim();
|
||||
if (tokenMint === "") {
|
||||
appendLogLine(logTextarea, "[ui] token mint is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const mintSignatureLimit = readPositiveIntegerInput(
|
||||
mintSignatureLimitInput,
|
||||
logTextarea,
|
||||
"mintSignatureLimit",
|
||||
);
|
||||
if (mintSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const poolSignatureLimit = readPositiveIntegerInput(
|
||||
mintPoolLimitInput,
|
||||
logTextarea,
|
||||
"poolSignatureLimit",
|
||||
);
|
||||
if (poolSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRoleText = httpRoleInput.value.trim();
|
||||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching token backfill for '${tokenMint}' with role '${httpRole ?? "history_backfill"}'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2BackfillTokenRequest = {
|
||||
tokenMint,
|
||||
httpRole,
|
||||
mintSignatureLimit,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2BackfillPayload>(
|
||||
"demo_pipeline2_backfill_token_mint",
|
||||
{ request },
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = payload.summaryJson;
|
||||
currentCatalog = payload.catalog;
|
||||
renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
|
||||
refreshPairSelect(payload.catalog, pairSelect);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] token backfill completed for '${payload.objectKey}'`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] token backfill error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
backfillPoolButton.addEventListener("click", async () => {
|
||||
const poolAddress = poolInput.value.trim();
|
||||
if (poolAddress === "") {
|
||||
appendLogLine(logTextarea, "[ui] pool address is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const poolSignatureLimit = readPositiveIntegerInput(
|
||||
poolSignatureLimitInput,
|
||||
logTextarea,
|
||||
"poolSignatureLimit",
|
||||
);
|
||||
if (poolSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRoleText = httpRoleInput.value.trim();
|
||||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching pool backfill for '${poolAddress}' with role '${httpRole ?? "history_backfill"}'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2BackfillPoolRequest = {
|
||||
poolAddress,
|
||||
httpRole,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2BackfillPayload>(
|
||||
"demo_pipeline2_backfill_pool_address",
|
||||
{ request },
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = payload.summaryJson;
|
||||
currentCatalog = payload.catalog;
|
||||
renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
|
||||
refreshPairSelect(payload.catalog, pairSelect);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] pool backfill completed for '${payload.objectKey}'`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] pool backfill error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
replayLocalPipelineButton.addEventListener("click", async () => {
|
||||
const replayLimit = readOptionalPositiveIntegerInput(
|
||||
replayLimitInput,
|
||||
logTextarea,
|
||||
"replayLimit",
|
||||
);
|
||||
if (replayLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenMetadataLimit = readOptionalPositiveIntegerInput(
|
||||
replayMetadataLimitInput,
|
||||
logTextarea,
|
||||
"tokenMetadataLimit",
|
||||
);
|
||||
if (tokenMetadataLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching local pipeline replay limit='${replayLimit ?? "none"}' metadata='${replayMetadataCheckbox.checked ? "yes" : "no"}'`,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await invoke<LocalPipelineReplayResult>(
|
||||
"demo_pipeline2_replay_local_pipeline",
|
||||
{
|
||||
limit: replayLimit,
|
||||
refreshMissingTokenMetadata: replayMetadataCheckbox.checked,
|
||||
tokenMetadataLimit,
|
||||
},
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = JSON.stringify(result, null, 2);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline replay completed: ${result.replayedTransactionCount.toString()} replayed, ${result.tradeEventCount.toString()} trades, ${result.pairCandleUpsertCount.toString()} candle upserts`,
|
||||
);
|
||||
|
||||
await refreshCatalog();
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline replay error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
diagnoseLocalPipelineButton.addEventListener("click", async () => {
|
||||
appendLogLine(logTextarea, "[ui] diagnosing local pipeline");
|
||||
|
||||
diagnoseLocalPipelineButton.disabled = true;
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2LocalDiagnosticsPayload>(
|
||||
"demo_pipeline2_diagnose_local_pipeline",
|
||||
);
|
||||
|
||||
safeLocalDiagnosticsTextarea.value = payload.summaryJson;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline diagnostics completed: ${payload.summary.decodedEventCount.toString()} decoded, ${payload.summary.tradeEventCount.toString()} trades, ${payload.summary.pairCandleCount.toString()} candles, actionableMissing='${payload.summary.actionableMissingTradeEventCount.toString()}', nonActionablePairs='${payload.summary.nonActionablePairCount.toString()}', blocking='${payload.summary.blockingIssueCount.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline diagnostics error: ${String(error)}`);
|
||||
} finally {
|
||||
diagnoseLocalPipelineButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
validateLocalPipelineButton.addEventListener("click", async () => {
|
||||
appendLogLine(logTextarea, "[ui] validating local pipeline with 0.7.27 profile");
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2LocalValidationPayload>(
|
||||
"demo_pipeline2_validate_local_pipeline",
|
||||
);
|
||||
|
||||
localValidationTextarea.value = payload.validationJson;
|
||||
localDiagnosticsTextarea.value = payload.summaryJson;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline validation completed: profile='${payload.run.validationProfileCode}' passed='${payload.run.validationPassed ? "yes" : "no"}' blocking='${payload.run.blockingIssueCount.toString()}' warnings='${payload.run.warningCount.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline validation error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
loadCandlesButton.addEventListener("click", async () => {
|
||||
const pairIdText = pairSelect.value.trim();
|
||||
if (pairIdText === "") {
|
||||
appendLogLine(logTextarea, "[ui] pair selection is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPairId = Number.parseInt(pairIdText, 10);
|
||||
if (Number.isNaN(parsedPairId) || parsedPairId <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid pair id '${pairIdText}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
let timeframeSeconds = Number.parseInt(timeframeSelect.value.trim(), 10);
|
||||
const customTimeframeText = customTimeframeInput.value.trim();
|
||||
if (customTimeframeText !== "") {
|
||||
const parsedCustom = Number.parseInt(customTimeframeText, 10);
|
||||
if (Number.isNaN(parsedCustom) || parsedCustom <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
|
||||
return;
|
||||
}
|
||||
timeframeSeconds = parsedCustom;
|
||||
}
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] loading candles for pair '${parsedPairId}' timeframe '${timeframeSeconds}s'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2PairCandlesRequest = {
|
||||
pairId: parsedPairId,
|
||||
timeframeSeconds,
|
||||
preferMaterialized: preferMaterializedInput.checked,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2PairCandlesPayload>(
|
||||
"demo_pipeline2_get_pair_candles",
|
||||
{ request },
|
||||
);
|
||||
|
||||
const candles = parseCandlesJson(payload.candlesJson);
|
||||
renderCandlesChart(
|
||||
chart,
|
||||
chartMeta,
|
||||
payload.pairId,
|
||||
payload.timeframeSeconds,
|
||||
candles,
|
||||
);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] loaded ${candles.length} candles for pair '${payload.pairId.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] load candles error: ${String(error)}`);
|
||||
setEmptyChart(chart, chartMeta, "Erreur lors du chargement des candles.");
|
||||
}
|
||||
});
|
||||
|
||||
await refreshCatalog();
|
||||
|
||||
if (currentCatalog !== null && currentCatalog.pairs.length > 0) {
|
||||
pairSelect.value = currentCatalog.pairs[0].pairId.toString();
|
||||
}
|
||||
});
|
||||
493
kb_demo_app/frontend/ts/demo_ws.ts
Normal file
493
kb_demo_app/frontend/ts/demo_ws.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_ws.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoWsEndpointSummary } from './bindings/DemoWsEndpointSummary.ts';
|
||||
import { DemoWsStatusPayload } from './bindings/DemoWsStatusPayload.ts';
|
||||
import { DemoWsSubscribeRequest } from './bindings/DemoWsSubscribeRequest.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
|
||||
function shortenLine(line: string, maxChars = 3000): string {
|
||||
if (line.length <= maxChars) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return `${line.slice(0, maxChars)} …[truncated ${line.length - maxChars} chars]`;
|
||||
}
|
||||
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
const safeLine = shortenLine(line, 3000);
|
||||
|
||||
const existingLines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
existingLines.push(`[${timestamp}] ${safeLine}`);
|
||||
|
||||
const maxLines = 800;
|
||||
const trimmedLines = existingLines.length > maxLines
|
||||
? existingLines.slice(existingLines.length - maxLines)
|
||||
: existingLines;
|
||||
|
||||
textarea.value = trimmedLines.join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatusBadge(badge: HTMLSpanElement, state: string): void {
|
||||
badge.textContent = state;
|
||||
|
||||
if (state === "Connected") {
|
||||
badge.className = "badge text-bg-success";
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Connecting" || state === "Disconnecting") {
|
||||
badge.className = "badge text-bg-warning";
|
||||
return;
|
||||
}
|
||||
|
||||
badge.className = "badge text-bg-secondary";
|
||||
}
|
||||
|
||||
function methodSupportsTypedMode(method: string): boolean {
|
||||
return ["account", "block", "logs", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsTarget(method: string): boolean {
|
||||
return ["account", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsFilter(method: string): boolean {
|
||||
return ["block", "logs"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsConfig(method: string): boolean {
|
||||
return ["account", "block", "logs", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function targetLabelForMethod(method: string): string {
|
||||
if (method === "account") return "Account pubkey";
|
||||
if (method === "program") return "Program id";
|
||||
if (method === "signature") return "Signature";
|
||||
return "Target";
|
||||
}
|
||||
|
||||
function methodPreset(method: string, mode: string): {
|
||||
target: string;
|
||||
filterJson: string;
|
||||
configJson: string;
|
||||
} {
|
||||
if (method === "account") {
|
||||
return {
|
||||
target: "11111111111111111111111111111111",
|
||||
filterJson: "",
|
||||
configJson: mode === "typed"
|
||||
? `{"encoding":"base64","commitment":"confirmed"}`
|
||||
: `{"encoding":"base64","commitment":"confirmed"}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "block") {
|
||||
return {
|
||||
target: "",
|
||||
filterJson: `{"mentionsAccountOrProgram":"11111111111111111111111111111111"}`,
|
||||
configJson: mode === "typed"
|
||||
? `{"commitment":"confirmed","encoding":"base64","transactionDetails":"signatures","showRewards":false,"maxSupportedTransactionVersion":0}`
|
||||
: `{"commitment":"confirmed","encoding":"base64","transactionDetails":"signatures","showRewards":false,"maxSupportedTransactionVersion":0}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "logs") {
|
||||
return {
|
||||
target: "",
|
||||
filterJson: `{"mentions":["11111111111111111111111111111111"]}`,
|
||||
configJson: `{"commitment":"confirmed"}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "program") {
|
||||
return {
|
||||
target: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
filterJson: "",
|
||||
configJson: mode === "typed"
|
||||
? `{"encoding":"base64","commitment":"confirmed","filters":[]}`
|
||||
: `{"encoding":"base64","commitment":"confirmed","filters":[]}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "signature") {
|
||||
return {
|
||||
target: "2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b",
|
||||
filterJson: "",
|
||||
configJson: `{"commitment":"confirmed","enableReceivedNotification":true}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
target: "",
|
||||
filterJson: "",
|
||||
configJson: "",
|
||||
};
|
||||
}
|
||||
|
||||
function updateFormVisibility(
|
||||
methodSelect: HTMLSelectElement,
|
||||
modeSelect: HTMLSelectElement,
|
||||
targetGroup: HTMLDivElement,
|
||||
targetLabel: HTMLLabelElement,
|
||||
targetInput: HTMLInputElement,
|
||||
filterGroup: HTMLDivElement,
|
||||
filterTextarea: HTMLTextAreaElement,
|
||||
configGroup: HTMLDivElement,
|
||||
configTextarea: HTMLTextAreaElement,
|
||||
): void {
|
||||
const method = methodSelect.value;
|
||||
const supportsTypedMode = methodSupportsTypedMode(method);
|
||||
|
||||
modeSelect.disabled = !supportsTypedMode;
|
||||
if (!supportsTypedMode) {
|
||||
modeSelect.value = "typed";
|
||||
}
|
||||
|
||||
targetGroup.style.display = methodNeedsTarget(method) ? "" : "none";
|
||||
filterGroup.style.display = methodNeedsFilter(method) ? "" : "none";
|
||||
configGroup.style.display = methodNeedsConfig(method) ? "" : "none";
|
||||
|
||||
targetLabel.textContent = targetLabelForMethod(method);
|
||||
|
||||
const preset = methodPreset(method, modeSelect.value);
|
||||
targetInput.value = preset.target;
|
||||
filterTextarea.value = preset.filterJson;
|
||||
configTextarea.value = preset.configJson;
|
||||
}
|
||||
|
||||
function applyStatusToUi(
|
||||
status: DemoWsStatusPayload,
|
||||
statusBadge: HTMLSpanElement,
|
||||
stateText: HTMLSpanElement,
|
||||
endpointText: HTMLSpanElement,
|
||||
subscriptionText: HTMLSpanElement,
|
||||
eventCountText: HTMLSpanElement,
|
||||
notificationCountText: HTMLSpanElement,
|
||||
uiLogCountText: HTMLSpanElement,
|
||||
suppressedLogCountText: HTMLSpanElement,
|
||||
lastEventKindText: HTMLSpanElement,
|
||||
connectButton: HTMLButtonElement,
|
||||
disconnectButton: HTMLButtonElement,
|
||||
subscribeButton: HTMLButtonElement,
|
||||
unsubscribeButton: HTMLButtonElement,
|
||||
): void {
|
||||
setStatusBadge(statusBadge, status.connectionState);
|
||||
stateText.textContent = status.connectionState;
|
||||
endpointText.textContent = status.endpointName && status.endpointUrl
|
||||
? `${status.endpointName} (${status.endpointUrl})`
|
||||
: "-";
|
||||
|
||||
if (status.currentSubscriptionId !== null) {
|
||||
subscriptionText.textContent =
|
||||
`${status.currentSubscribeMethod ?? "?"} / #${status.currentSubscriptionId} / ${status.currentNotificationMethod ?? "?"}`;
|
||||
} else {
|
||||
subscriptionText.textContent = "-";
|
||||
}
|
||||
|
||||
eventCountText.textContent = String(status.eventCountTotal);
|
||||
notificationCountText.textContent = String(status.notificationCountTotal);
|
||||
uiLogCountText.textContent = String(status.uiLogCount);
|
||||
suppressedLogCountText.textContent = String(status.suppressedLogCount);
|
||||
lastEventKindText.textContent = status.lastEventKind ?? "-";
|
||||
|
||||
const isConnected = status.connectionState === "Connected";
|
||||
const isBusy = status.connectionState === "Connecting" || status.connectionState === "Disconnecting";
|
||||
|
||||
connectButton.disabled = isConnected || isBusy;
|
||||
disconnectButton.disabled = !isConnected && status.connectionState !== "Disconnecting";
|
||||
subscribeButton.disabled = !isConnected || isBusy;
|
||||
unsubscribeButton.disabled = !isConnected || isBusy || status.currentSubscriptionId === null;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
|
||||
debug("demo_ws window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const endpointSelect = document.querySelector<HTMLSelectElement>("#demoWsEndpointSelect");
|
||||
const methodSelect = document.querySelector<HTMLSelectElement>("#demoWsMethodSelect");
|
||||
const modeSelect = document.querySelector<HTMLSelectElement>("#demoWsModeSelect");
|
||||
const targetGroup = document.querySelector<HTMLDivElement>("#demoWsTargetGroup");
|
||||
const targetLabel = document.querySelector<HTMLLabelElement>("#demoWsTargetLabel");
|
||||
const targetInput = document.querySelector<HTMLInputElement>("#demoWsTargetInput");
|
||||
const filterGroup = document.querySelector<HTMLDivElement>("#demoWsFilterGroup");
|
||||
const filterTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsFilterTextarea");
|
||||
const configGroup = document.querySelector<HTMLDivElement>("#demoWsConfigGroup");
|
||||
const configTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsConfigTextarea");
|
||||
const statusBadge = document.querySelector<HTMLSpanElement>("#demoWsStatusBadge");
|
||||
const stateText = document.querySelector<HTMLSpanElement>("#demoWsStateText");
|
||||
const endpointText = document.querySelector<HTMLSpanElement>("#demoWsEndpointText");
|
||||
const subscriptionText = document.querySelector<HTMLSpanElement>("#demoWsSubscriptionText");
|
||||
const requestText = document.querySelector<HTMLSpanElement>("#demoWsRequestText");
|
||||
const eventCountText = document.querySelector<HTMLSpanElement>("#demoWsEventCountText");
|
||||
const notificationCountText = document.querySelector<HTMLSpanElement>("#demoWsNotificationCountText");
|
||||
const uiLogCountText = document.querySelector<HTMLSpanElement>("#demoWsUiLogCountText");
|
||||
const suppressedLogCountText = document.querySelector<HTMLSpanElement>("#demoWsSuppressedLogCountText");
|
||||
const lastEventKindText = document.querySelector<HTMLSpanElement>("#demoWsLastEventKindText");
|
||||
const connectButton = document.querySelector<HTMLButtonElement>("#demoWsConnectButton");
|
||||
const disconnectButton = document.querySelector<HTMLButtonElement>("#demoWsDisconnectButton");
|
||||
const subscribeButton = document.querySelector<HTMLButtonElement>("#demoWsSubscribeButton");
|
||||
const unsubscribeButton = document.querySelector<HTMLButtonElement>("#demoWsUnsubscribeButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsClearLogButton");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsLogTextarea");
|
||||
|
||||
if (
|
||||
!eventCountText || !notificationCountText || !uiLogCountText || !suppressedLogCountText || !lastEventKindText ||
|
||||
!endpointSelect || !methodSelect || !modeSelect || !targetGroup || !targetLabel || !targetInput ||
|
||||
!filterGroup || !filterTextarea || !configGroup || !configTextarea || !statusBadge ||
|
||||
!stateText || !endpointText || !subscriptionText || !requestText || !connectButton ||
|
||||
!disconnectButton || !subscribeButton || !unsubscribeButton || !clearLogButton || !logTextarea
|
||||
) {
|
||||
debug("demo_ws UI controls not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let unlistenLogEvent: UnlistenFn | null = null;
|
||||
let unlistenStatusEvent: UnlistenFn | null = null;
|
||||
|
||||
try {
|
||||
unlistenLogEvent = await listen<string>("demo-ws-log", (event) => {
|
||||
appendLogLine(logTextarea, event.payload);
|
||||
});
|
||||
|
||||
unlistenStatusEvent = await listen<DemoWsStatusPayload>("demo-ws-status", (event) => {
|
||||
applyStatusToUi(
|
||||
event.payload,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] event listen error: ${String(error)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoints = await invoke<DemoWsEndpointSummary[]>("demo_ws_list_endpoints");
|
||||
|
||||
endpointSelect.innerHTML = "";
|
||||
for (const endpoint of endpoints) {
|
||||
const option = document.createElement("option");
|
||||
option.value = endpoint.name;
|
||||
option.textContent = `${endpoint.name} — ${endpoint.provider} — ${endpoint.resolvedUrl}`;
|
||||
option.disabled = !endpoint.enabled;
|
||||
endpointSelect.appendChild(option);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] endpoint list error: ${String(error)}`);
|
||||
}
|
||||
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
|
||||
methodSelect.addEventListener("change", () => {
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
});
|
||||
|
||||
modeSelect.addEventListener("change", () => {
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_get_status");
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] initial status error: ${String(error)}`);
|
||||
}
|
||||
|
||||
appendLogLine(logTextarea, "[ui] demo_ws window loaded");
|
||||
|
||||
connectButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_connect", {
|
||||
endpointName: endpointSelect.value,
|
||||
});
|
||||
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] connect error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
disconnectButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_disconnect");
|
||||
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] disconnect error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
subscribeButton.addEventListener("click", async () => {
|
||||
const request: DemoWsSubscribeRequest = {
|
||||
method: methodSelect.value,
|
||||
mode: modeSelect.value,
|
||||
target: targetInput.value.trim() === "" ? null : targetInput.value.trim(),
|
||||
filterJson: filterTextarea.value.trim() === "" ? null : filterTextarea.value.trim(),
|
||||
configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(),
|
||||
};
|
||||
|
||||
try {
|
||||
const requestId = await invoke<number>("demo_ws_subscribe", { request });
|
||||
requestText.textContent = `request_id=${requestId}`;
|
||||
appendLogLine(logTextarea, `[ui] subscribe request sent: request_id=${requestId}`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] subscribe error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
unsubscribeButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const requestId = await invoke<number>("demo_ws_unsubscribe_current");
|
||||
requestText.textContent = `unsubscribe_request_id=${requestId}`;
|
||||
appendLogLine(logTextarea, `[ui] unsubscribe request sent: request_id=${requestId}`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] unsubscribe error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (unlistenLogEvent) {
|
||||
unlistenLogEvent();
|
||||
}
|
||||
|
||||
if (unlistenStatusEvent) {
|
||||
unlistenStatusEvent();
|
||||
}
|
||||
});
|
||||
});
|
||||
224
kb_demo_app/frontend/ts/demo_ws_manager.ts
Normal file
224
kb_demo_app/frontend/ts/demo_ws_manager.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_ws_manager.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoWsManagerSnapshotPayload } from './bindings/DemoWsManagerSnapshotPayload.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
|
||||
const endpointCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerEndpointCountText");
|
||||
const startedCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerStartedCountText");
|
||||
const roleSelect = document.querySelector<HTMLSelectElement>("#demoWsManagerRoleSelect");
|
||||
const tableBody = document.querySelector<HTMLTableSectionElement>("#demoWsManagerTableBody");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsManagerLogTextarea");
|
||||
const startAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartAllButton");
|
||||
const stopAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopAllButton");
|
||||
const refreshButton = document.querySelector<HTMLButtonElement>("#demoWsManagerRefreshButton");
|
||||
const startRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartRoleButton");
|
||||
const stopRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopRoleButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsManagerClearLogButton");
|
||||
|
||||
function appendLogLine(line: string): void {
|
||||
if (!logTextarea) {
|
||||
return;
|
||||
}
|
||||
const prefix = logTextarea.value.length > 0 ? "\n" : "";
|
||||
logTextarea.value += `${prefix}${line}`;
|
||||
logTextarea.scrollTop = logTextarea.scrollHeight;
|
||||
}
|
||||
|
||||
function renderSnapshot(snapshot: DemoWsManagerSnapshotPayload): void {
|
||||
if (endpointCountText) {
|
||||
endpointCountText.textContent = String(snapshot.endpointCount);
|
||||
}
|
||||
if (startedCountText) {
|
||||
startedCountText.textContent = String(snapshot.startedCount);
|
||||
}
|
||||
if (!tableBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
for (const endpoint of snapshot.endpoints) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div class="fw-semibold">${endpoint.name}</div>
|
||||
<div class="small text-body-secondary">${endpoint.resolvedUrl}</div>
|
||||
</td>
|
||||
<td>${endpoint.provider}</td>
|
||||
<td>${endpoint.roles.join(", ")}</td>
|
||||
<td>${endpoint.connectionState}</td>
|
||||
<td>${endpoint.activeSubscriptionCount}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSnapshot(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_get_snapshot");
|
||||
renderSnapshot(snapshot);
|
||||
appendLogLine("[ui] refreshed manager snapshot");
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] snapshot error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles(): Promise<void> {
|
||||
if (!roleSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleSelect.innerHTML = "";
|
||||
|
||||
try {
|
||||
const roles = await invoke<string[]>("demo_ws_manager_list_roles");
|
||||
for (const role of roles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = role;
|
||||
option.textContent = role;
|
||||
roleSelect.appendChild(option);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] list roles error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startAll(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_all");
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] start all error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopAll(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_all");
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] stop all error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startRole(): Promise<void> {
|
||||
if (!roleSelect || roleSelect.value.trim().length === 0) {
|
||||
appendLogLine("[ui] no role selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_role", {
|
||||
role: roleSelect.value,
|
||||
});
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] start role error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRole(): Promise<void> {
|
||||
if (!roleSelect || roleSelect.value.trim().length === 0) {
|
||||
appendLogLine("[ui] no role selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_role", {
|
||||
role: roleSelect.value,
|
||||
});
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] stop role error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_ws_manager window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
if (startAllButton) {
|
||||
startAllButton.addEventListener("click", () => {
|
||||
void startAll();
|
||||
});
|
||||
}
|
||||
if (stopAllButton) {
|
||||
stopAllButton.addEventListener("click", () => {
|
||||
void stopAll();
|
||||
});
|
||||
}
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener("click", () => {
|
||||
void refreshSnapshot();
|
||||
});
|
||||
}
|
||||
if (startRoleButton) {
|
||||
startRoleButton.addEventListener("click", () => {
|
||||
void startRole();
|
||||
});
|
||||
}
|
||||
if (stopRoleButton) {
|
||||
stopRoleButton.addEventListener("click", () => {
|
||||
void stopRole();
|
||||
});
|
||||
}
|
||||
if (clearLogButton && logTextarea) {
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
await listen<string>("kb-demo-ws-manager-log", (event) => {
|
||||
appendLogLine(event.payload);
|
||||
});
|
||||
|
||||
await listen<DemoWsManagerSnapshotPayload>("kb-demo-ws-manager-snapshot", (event) => {
|
||||
renderSnapshot(event.payload);
|
||||
});
|
||||
|
||||
await loadRoles();
|
||||
await refreshSnapshot();
|
||||
});
|
||||
161
kb_demo_app/frontend/ts/main.ts
Normal file
161
kb_demo_app/frontend/ts/main.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: kb_demo_app/frontend/ts/main.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
async function openDemoWsWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_ws_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_ws_window failed:", error);
|
||||
}
|
||||
}
|
||||
async function openDemoHttpWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_http_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_http_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoWsManagerWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_ws_manager_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_ws_manager_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoPipelineWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_pipeline_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_pipeline_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoPipeline2Window(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_pipeline2_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_pipeline2_window2 failed:", error);
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
|
||||
debug("main window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const openDemoWsButton = document.querySelector<HTMLButtonElement>("#openDemoWsButton");
|
||||
const openDemoWsButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsButtonSecondary");
|
||||
|
||||
const openDemoHttpButton = document.querySelector<HTMLButtonElement>("#openDemoHttpButton");
|
||||
const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary");
|
||||
const openDemoWsManagerButton = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButton");
|
||||
const openDemoWsManagerButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButtonSecondary");
|
||||
const openDemoPipelineButton = document.querySelector<HTMLButtonElement>("#openDemoPipelineButton");
|
||||
const openDemoPipelineButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoPipelineButtonSecondary");
|
||||
const openDemoPipeline2Button = document.querySelector<HTMLButtonElement>("#openDemoPipeline2Button");
|
||||
const openDemoPipeline2ButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoPipeline2ButtonSecondary");
|
||||
|
||||
if (openDemoWsButton) {
|
||||
openDemoWsButton.addEventListener("click", () => {
|
||||
void openDemoWsWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsButtonSecondary) {
|
||||
openDemoWsButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoWsWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoHttpButton) {
|
||||
openDemoHttpButton.addEventListener("click", () => {
|
||||
void openDemoHttpWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoHttpButtonSecondary) {
|
||||
openDemoHttpButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoHttpWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsManagerButton) {
|
||||
openDemoWsManagerButton.addEventListener("click", () => {
|
||||
void openDemoWsManagerWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsManagerButtonSecondary) {
|
||||
openDemoWsManagerButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoWsManagerWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipelineButton) {
|
||||
openDemoPipelineButton.addEventListener("click", () => {
|
||||
void openDemoPipelineWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipelineButtonSecondary) {
|
||||
openDemoPipelineButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoPipelineWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2Button) {
|
||||
openDemoPipeline2Button.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2ButtonSecondary) {
|
||||
openDemoPipeline2ButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
95
kb_demo_app/frontend/ts/splash.ts
Normal file
95
kb_demo_app/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// file: kb_demo_app/frontend/ts/splash.ts
|
||||
|
||||
import { error, info, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { SplashOrder } from './bindings/SplashOrder.ts';
|
||||
|
||||
// Fonction d'animation d'opacité
|
||||
async function animateOpacity(
|
||||
element: HTMLElement,
|
||||
fromOpacity: number,
|
||||
toOpacity: number,
|
||||
durationMs: number
|
||||
): Promise<void> {
|
||||
console.log(`Animating from ${fromOpacity} to ${toOpacity} over ${durationMs}ms`);
|
||||
//debug(`Animating from ${fromOpacity} to ${toOpacity} over ${durationMs}ms`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const startTime = performance.now();
|
||||
const startOpacity = fromOpacity;
|
||||
const changeOpacity = toOpacity - fromOpacity;
|
||||
|
||||
function update(currentTime: number) {
|
||||
const elapsed = currentTime - startTime;
|
||||
if (elapsed >= durationMs) {
|
||||
element.style.opacity = toOpacity.toString();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = elapsed / durationMs;
|
||||
element.style.opacity = (startOpacity + changeOpacity * progress).toString();
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
}
|
||||
|
||||
// Journalisation
|
||||
function addLogMessage(message: string): void {
|
||||
console.log(`Splash: ${message}`);
|
||||
const debugInfo = document.getElementById('debug-info');
|
||||
if (debugInfo) {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
let msg = `${time}: ${message}<br>`;
|
||||
debugInfo.innerHTML += msg;
|
||||
}
|
||||
}
|
||||
|
||||
// Pour ajouter des messages directement (sans événements)
|
||||
function addMessage(message: string, status: string): void {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `splash-message ${status}`;
|
||||
messageElement.textContent = message;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
listen("splash", (event) => {
|
||||
const splashorder = event.payload as SplashOrder;
|
||||
if (splashorder.order == "fadein") {
|
||||
const container = document.getElementById('splash-container');
|
||||
if (container) {
|
||||
animateOpacity(container, 0, 1, 3000);
|
||||
} else {
|
||||
error("no container");
|
||||
}
|
||||
} else if (splashorder.order == "fadeout") {
|
||||
const container = document.getElementById('splash-container');
|
||||
if (container) {
|
||||
animateOpacity(container, 1, 0, 3000);
|
||||
} else {
|
||||
error("no container");
|
||||
}
|
||||
} else if (splashorder.order == "add_msg" && splashorder.msg && splashorder.status) {
|
||||
addMessage(splashorder.msg, splashorder.status);
|
||||
} else if (splashorder.order == "add_log" && splashorder.msg) {
|
||||
addLogMessage(splashorder.msg);
|
||||
} else {
|
||||
error("unknown order:" + splashorder.order);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Démarrer l'initialisation au chargement du DOM
|
||||
//window.addEventListener('DOMContentLoaded', initialize);
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
void takeoverConsole();
|
||||
info("window loaded");
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user