0.7.27 +Refactor

This commit is contained in:
2026-05-10 00:33:01 +02:00
parent cb2e8e7096
commit 1f0137b9de
261 changed files with 12308 additions and 8928 deletions

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

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

View File

@@ -0,0 +1,27 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { 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, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request payload for pool backfill.
*/
export type DemoPipeline2BackfillPoolRequest = {
/**
* Pool address to backfill.
*/
poolAddress: string,
/**
* Optional HTTP role.
*/
httpRole: string | null,
/**
* Limit for signatures fetched from the pool.
*/
poolSignatureLimit: number, };

View File

@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request payload for token backfill.
*/
export type 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, };

View File

@@ -0,0 +1,25 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { 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>, };

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 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, };

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Candle payload returned to the UI.
*/
export type DemoPipeline2PairCandlesPayload = {
/**
* Pair id.
*/
pairId: number,
/**
* Timeframe in seconds.
*/
timeframeSeconds: number,
/**
* Pretty JSON array of candles.
*/
candlesJson: string, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request payload for pair candles.
*/
export type DemoPipeline2PairCandlesRequest = {
/**
* Pair id to load.
*/
pairId: number,
/**
* Timeframe in seconds.
*/
timeframeSeconds: number,
/**
* Whether materialized candles should be preferred when available.
*/
preferMaterialized: boolean, };

View File

@@ -0,0 +1,30 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* One pair item for the local catalog.
*/
export type 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, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* One pool item for the local catalog.
*/
export type DemoPipeline2PoolItem = {
/**
* Pool address.
*/
poolAddress: string,
/**
* Optional internal pair id when known.
*/
pairId: number | null,
/**
* Optional DEX code.
*/
dexCode: string | null, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* One token item for the local catalog.
*/
export type DemoPipeline2TokenItem = {
/**
* Token mint.
*/
mint: string,
/**
* Optional token symbol.
*/
symbol: string | null,
/**
* Optional token name.
*/
name: string | null, };

View File

@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Response payload for one pool backfill launched from `kb_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, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request payload for one pool backfill launched from `kb_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, };

View File

@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Response payload for one 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, };

View File

@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request payload for 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, };

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.
/**
* Endpoint summary sent to the demo frontend.
*/
export type DemoWsEndpointSummary = { name: string, resolvedUrl: string, provider: string, enabled: boolean, roles: Array<string>, };

View 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.
/**
* Static endpoint summary enriched with current manager state.
*/
export type DemoWsManagerEndpointSummary = { name: string, resolvedUrl: string, provider: string, roles: Array<string>, connectionState: string, activeSubscriptionCount: number, };

View File

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

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

View 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.
/**
* Subscribe request sent by the demo frontend.
*/
export type DemoWsSubscribeRequest = { method: string, mode: string, target: string | null, filterJson: string | null, configJson: string | null, };

View File

@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 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, };

View 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 dune transaction.";
if (method === "sendTransaction") {
return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion derreur.";
}
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 denvoi. 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);
});

File diff suppressed because it is too large Load Diff

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

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

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

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

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