0.3.15-pre.011

This commit is contained in:
2026-09-16 09:07:31 +02:00
parent e919945d1e
commit a248be04ca
12 changed files with 807 additions and 31 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 619 # version: 620
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
[workspace.package] [workspace.package]
version = "0.3.15-pre.10.fix.1" version = "0.3.15-pre.11"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md --> <!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 11 --> <!-- version: 12 -->
# `ksp-app-raw-transaction-ingest-desk` # `ksp-app-raw-transaction-ingest-desk`
@@ -48,7 +48,7 @@ http-block-polling
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime. Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. L'UI détaillée de supervision reste la tranche `pre.011`. Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker.
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées. Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md --> <!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 10 --> <!-- version: 11 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk` # Utilisation de `ksp-app-raw-transaction-ingest-desk`
@@ -42,7 +42,7 @@ Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `S
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert. Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. L'affichage frontend complet et les contrôles de refresh dédiés sont finalisés en `pre.011`. Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée. Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html --> <!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
<!-- version: 6 --> <!-- version: 7 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -23,6 +23,7 @@
</div> </div>
<div class="d-flex align-items-center gap-2 ms-auto"> <div class="d-flex align-items-center gap-2 ms-auto">
<span id="headerProfile" class="badge text-bg-light border text-dark"></span> <span id="headerProfile" class="badge text-bg-light border text-dark"></span>
<span id="headerStore" class="badge text-bg-secondary">Store idle</span>
<span id="headerPhase" class="badge text-bg-secondary">inventory</span> <span id="headerPhase" class="badge text-bg-secondary">inventory</span>
</div> </div>
</div> </div>
@@ -53,9 +54,14 @@
<h1 class="h3 mb-1">Routes live</h1> <h1 class="h3 mb-1">Routes live</h1>
<p class="text-body-secondary mb-0">Inventaire Config et runtime multi-route avec un Worker indépendant par route et Store partagé same-network.</p> <p class="text-body-secondary mb-0">Inventaire Config et runtime multi-route avec un Worker indépendant par route et Store partagé same-network.</p>
</div> </div>
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button"> <div class="d-flex gap-2 flex-wrap">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh <button id="refreshRouteMonitoring" class="btn btn-outline-secondary btn-sm" type="button">
</button> <i class="fa-solid fa-wave-square me-2" aria-hidden="true"></i>Resync monitoring
</button>
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh inventory
</button>
</div>
</div> </div>
<div class="row g-3 align-items-end mb-4"> <div class="row g-3 align-items-end mb-4">
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
@@ -81,6 +87,7 @@
<div class="alert alert-info" role="status"> <div class="alert alert-info" role="status">
<strong>Configured</strong> signifie que Config prouve la composabilité. Start revalide Config puis lance un Worker indépendant. Les routes du même réseau partagent le Store ; chaque fault reste isolé à sa route. <strong>Configured</strong> signifie que Config prouve la composabilité. Start revalide Config puis lance un Worker indépendant. Les routes du même réseau partagent le Store ; chaque fault reste isolé à sa route.
</div> </div>
<div id="routeCommandFeedback" class="alert alert-info" role="status" aria-live="polite" hidden></div>
<div class="card border-primary-subtle mb-4"> <div class="card border-primary-subtle mb-4">
<div class="card-body d-flex align-items-center justify-content-between gap-3 flex-wrap"> <div class="card-body d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div> <div>
@@ -94,6 +101,89 @@
</div> </div>
<div class="small text-body-secondary mb-3">Réseau affiché : <span id="routeSelectedProfile" class="app-route-code"></span></div> <div class="small text-body-secondary mb-3">Réseau affiché : <span id="routeSelectedProfile" class="app-route-code"></span></div>
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div> <div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
<section id="routeMonitoringDetail" class="card shadow-sm mt-4 app-monitoring-detail" aria-live="polite" hidden>
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="fw-semibold">Supervision route</div>
<div id="monitorRouteId" class="small text-body-secondary app-route-code"></div>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<span id="monitorLifecycle" class="badge text-bg-secondary"></span>
<span id="monitorHealthBadge" class="badge text-bg-secondary"></span>
<span id="monitorActivityBadge" class="badge text-bg-light border text-dark"></span>
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Network</div><div id="monitorNetwork" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Commitment</div><div id="monitorCommitment" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Sequence</div><div id="monitorSequence" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Fault</div><div id="monitorFault" class="app-route-code fw-semibold"></div></div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Pipeline / persistence</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Health</dt><dd id="monitorHealth" class="col-5"></dd>
<dt class="col-7">Activity</dt><dd id="monitorActivity" class="col-5"></dd>
<dt class="col-7">Admission queue</dt><dd id="monitorAdmissionQueue" class="col-5"></dd>
<dt class="col-7">Persistence in-flight</dt><dd id="monitorPersistence" class="col-5"></dd>
<dt class="col-7">Admitted</dt><dd id="monitorAdmittedTotal" class="col-5"></dd>
<dt class="col-7">Canonicalized</dt><dd id="monitorCanonicalizedTotal" class="col-5"></dd>
<dt class="col-7">Persisted</dt><dd id="monitorPersistedTotal" class="col-5"></dd>
<dt class="col-7">RAW inserted</dt><dd id="monitorEntityInsertedTotal" class="col-5"></dd>
<dt class="col-7">RAW already present</dt><dd id="monitorEntityAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">RAW purged skipped</dt><dd id="monitorEntitySkippedPurgedTotal" class="col-5"></dd>
<dt class="col-7">Observations inserted</dt><dd id="monitorObservationInsertedTotal" class="col-5"></dd>
<dt class="col-7">Observations already present</dt><dd id="monitorObservationAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">Content conflicts</dt><dd id="monitorContentConflictTotal" class="col-5"></dd>
<dt class="col-7">Store failures</dt><dd id="monitorStoreFailureTotal" class="col-5"></dd>
<dt class="col-7">Source failures</dt><dd id="monitorSourceFailureTotal" class="col-5"></dd>
<dt class="col-7">Backpressure waits</dt><dd id="monitorBackpressureWaitTotal" class="col-5"></dd>
<dt class="col-7">Hydration pending</dt><dd id="monitorHydrationPending" class="col-5"></dd>
<dt class="col-7">Processing frontier</dt><dd id="monitorProcessingFrontier" class="col-5"></dd>
<dt class="col-7">Oldest pending slot</dt><dd id="monitorOldestPending" class="col-5"></dd>
</dl>
</div>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Sources / continuité / repair</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Source state</dt><dd id="monitorSourceState" class="col-5"></dd>
<dt class="col-7">Source counts</dt><dd id="monitorSourceCounts" class="col-5"></dd>
<dt class="col-7">Reconnects</dt><dd id="monitorSourceReconnectTotal" class="col-5"></dd>
<dt class="col-7">Replay attempts</dt><dd id="monitorSourceReplayAttemptTotal" class="col-5"></dd>
<dt class="col-7">Continuity gaps source</dt><dd id="monitorSourceContinuityGapTotal" class="col-5"></dd>
<dt class="col-7">Policy observed</dt><dd id="monitorContinuityPolicyObserved" class="col-5"></dd>
<dt class="col-7">Continuity frontier</dt><dd id="monitorContinuityFrontier" class="col-5"></dd>
<dt class="col-7">Open gaps</dt><dd id="monitorContinuityHasOpenGaps" class="col-5"></dd>
<dt class="col-7">Failed losses reconciled</dt><dd id="monitorFailedSourceLossesReconciled" class="col-5"></dd>
<dt class="col-7">Future coverage</dt><dd id="monitorFutureTargetCoverage" class="col-5"></dd>
<dt class="col-7">Open / repairing</dt><dd class="col-5"><span id="monitorOpenGapCount"></span> / <span id="monitorRepairingGapCount"></span></dd>
<dt class="col-7">Repaired / unresolved</dt><dd class="col-5"><span id="monitorRepairedGapTotal"></span> / <span id="monitorUnresolvedGapTotal"></span></dd>
<dt class="col-7">Oldest open gap</dt><dd id="monitorOldestOpenGap" class="col-5"></dd>
<dt class="col-7">Replay repair</dt><dd id="monitorReplayRepairTotal" class="col-5"></dd>
<dt class="col-7">Redundant coverage repair</dt><dd id="monitorRedundantCoverageRepairTotal" class="col-5"></dd>
<dt class="col-7">HTTP scan repair</dt><dd id="monitorHttpScanRepairTotal" class="col-5"></dd>
<dt class="col-7">Block fetch repair</dt><dd id="monitorRepairBlockFetchTotal" class="col-5"></dd>
<dt class="col-7">Transaction hydration repair</dt><dd id="monitorRepairTransactionHydrationTotal" class="col-5"></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="mt-3">
<div class="fw-semibold mb-2">Gaps courants / récents</div>
<div id="monitorGapList" class="list-group app-monitoring-gaps"></div>
</div>
</div>
</section>
</section> </section>
<section data-view-panel="diagnostics" hidden> <section data-view-panel="diagnostics" hidden>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss // file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss
// version: 2 // version: 3
$app-header-height: 72px; $app-header-height: 72px;
$app-footer-height: 42px; $app-footer-height: 42px;
@@ -110,6 +110,35 @@ body {
word-break: break-word; word-break: break-word;
} }
.app-route-monitoring-summary {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.app-monitoring-detail {
min-width: 0;
overflow: hidden;
}
.app-monitoring-list dt,
.app-monitoring-list dd {
margin-bottom: 0.45rem;
}
.app-monitoring-list dd,
.app-monitoring-gaps,
.app-monitoring-detail .app-route-code {
font-family: var(--bs-font-monospace);
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-monitoring-gaps .list-group-item {
min-width: 0;
}
.app-placeholder { .app-placeholder {
min-height: 320px; min-height: 320px;
display: flex; display: flex;

View File

@@ -1,9 +1,10 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts // file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
// version: 6 // version: 7
import "bootstrap"; import "bootstrap";
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
import "simplebar"; import "simplebar";
import { listen } from "@tauri-apps/api/event";
import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts"; import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
import type { RawIngestCommitment } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts"; import type { RawIngestCommitment } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts";
import type { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts"; import type { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts";
@@ -11,6 +12,7 @@ import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transact
import type { RawIngestRouteRuntimeDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts"; import type { RawIngestRouteRuntimeDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts";
import type { RawIngestRouteStartRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts"; import type { RawIngestRouteStartRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts";
import type { RawIngestRouteStopRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts"; import type { RawIngestRouteStopRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts";
import type { RawIngestRouteMonitoringDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log"; import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
@@ -26,8 +28,10 @@ const viewTitles: Record<ViewId, string> = {
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>(); const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>(); const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
let routeInventory: RawIngestRouteInventoryDto | null = null; let routeInventory: RawIngestRouteInventoryDto | null = null;
let selectedProfileId: string | null = null; let selectedProfileId: string | null = null;
let selectedMonitoringKey: string | null = null;
function isViewId(value: string | undefined): value is ViewId { function isViewId(value: string | undefined): value is ViewId {
return value === "routes" || value === "diagnostics"; return value === "routes" || value === "diagnostics";
@@ -108,6 +112,95 @@ function runtimeKey(profileId: string, routeId: RawIngestRouteId): string {
return `${profileId}::${routeId}`; return `${profileId}::${routeId}`;
} }
function lifecycleBadgeClass(state: string): string {
if (state === "faulted") {
return "badge text-bg-danger";
}
if (state === "running") {
return "badge text-bg-success";
}
if (state === "starting" || state === "stopping") {
return "badge text-bg-primary";
}
if (state === "configured") {
return "badge text-bg-info";
}
return "badge text-bg-secondary";
}
function yesNo(value: boolean): string {
return value ? "oui" : "non";
}
function optionalText(value: string | null): string {
return value ?? "—";
}
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
return fallback;
}
function renderRouteFeedback(message: string, level: "danger" | "info" | "success"): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = false;
feedback.classList.remove("alert-danger", "alert-info", "alert-success");
feedback.classList.add(`alert-${level}`);
feedback.textContent = message;
}
function clearRouteFeedback(): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = true;
feedback.textContent = "";
}
function monitoringRuntime(status: RawIngestRouteMonitoringDto): RawIngestRouteRuntimeDto {
return {
commitment: status.commitment,
inventoryGeneration: status.inventoryGeneration,
network: status.network,
profileId: status.profileId,
routeId: status.routeId,
state: status.state,
};
}
function recordRouteMonitoring(status: RawIngestRouteMonitoringDto): void {
const key = runtimeKey(status.profileId, status.routeId);
const runtime = monitoringRuntime(status);
routeMonitoring.set(key, status);
lastRuntimes.set(key, runtime);
if (runtimeIsActive(runtime)) {
activeRuntimes.set(key, runtime);
} else {
activeRuntimes.delete(key);
}
}
function selectRouteMonitoring(profileId: string, routeId: RawIngestRouteId, family: string, source: "event" | "user"): void {
const key = runtimeKey(profileId, routeId);
selectedMonitoringKey = key;
renderMonitoringDetail();
frontendDebug("main", "Raw Transaction Ingest Desk route supervision selected", {
family,
routeId,
selected: routeMonitoring.has(key),
source,
});
}
function renderSelectedProfile(): void { function renderSelectedProfile(): void {
if (routeInventory === null) { if (routeInventory === null) {
return; return;
@@ -126,24 +219,33 @@ function renderSelectedProfile(): void {
for (const route of profile.routes) { for (const route of profile.routes) {
const key = runtimeKey(profile.profileId, route.routeId); const key = runtimeKey(profile.profileId, route.routeId);
const runtime = activeRuntimes.get(key) ?? lastRuntimes.get(key) ?? null; const runtime = activeRuntimes.get(key) ?? lastRuntimes.get(key) ?? null;
const active = runtime !== null && runtimeIsActive(runtime); const monitoring = routeMonitoring.get(key) ?? null;
const projectedState = monitoring?.state ?? runtime?.state ?? route.state;
const active = projectedState === "starting" || projectedState === "running" || projectedState === "stopping";
const column = document.createElement("div"); const column = document.createElement("div");
column.className = "col-12 col-xl-6 app-route-column"; column.className = "col-12 col-xl-6 app-route-column";
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "card h-100 shadow-sm app-route-card"; card.className = "card h-100 shadow-sm app-route-card";
const body = document.createElement("div"); const body = document.createElement("div");
body.className = "card-body"; body.className = "card-body";
const heading = document.createElement("div");
heading.className = "d-flex align-items-start justify-content-between gap-3";
const headingText = document.createElement("div");
const title = document.createElement("h2"); const title = document.createElement("h2");
title.className = "h6 mb-2"; title.className = "h6 mb-1";
title.textContent = route.label; title.textContent = route.label;
const code = document.createElement("div"); const code = document.createElement("div");
code.className = "app-route-code text-body-secondary small"; code.className = "app-route-code text-body-secondary small";
code.textContent = route.routeId; code.textContent = route.routeId;
headingText.append(title, code);
const family = document.createElement("span");
family.className = "badge text-bg-light border text-dark";
family.textContent = route.family;
heading.append(headingText, family);
const meta = document.createElement("div"); const meta = document.createElement("div");
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta"; meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
const state = document.createElement("span"); const state = document.createElement("span");
const projectedState = runtime?.state ?? route.state; state.className = lifecycleBadgeClass(projectedState);
state.className = projectedState === "faulted" ? "badge text-bg-danger" : active ? "badge text-bg-primary" : route.selectable ? "badge text-bg-success" : "badge text-bg-secondary";
state.textContent = projectedState; state.textContent = projectedState;
const network = document.createElement("span"); const network = document.createElement("span");
network.className = "badge text-bg-light border text-dark"; network.className = "badge text-bg-light border text-dark";
@@ -152,10 +254,31 @@ function renderSelectedProfile(): void {
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning"; reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
reason.textContent = reasonLabel(route.reason); reason.textContent = reasonLabel(route.reason);
meta.append(state, network, reason); meta.append(state, network, reason);
body.append(title, code, meta); body.append(heading, meta);
if (monitoring) {
const summary = document.createElement("div");
summary.className = "app-route-monitoring-summary mt-3";
const health = document.createElement("span");
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = `health: ${monitoring.health}`;
const activity = document.createElement("span");
activity.className = "badge text-bg-light border text-dark";
activity.textContent = `activity: ${monitoring.activity}`;
const persisted = document.createElement("span");
persisted.className = "badge text-bg-light border text-dark";
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
const sources = document.createElement("span");
sources.className = "badge text-bg-light border text-dark";
sources.textContent = `sources: ${monitoring.sourceActive}/${monitoring.sourceTotal}`;
const gaps = document.createElement("span");
gaps.className = monitoring.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
summary.append(health, activity, persisted, sources, gaps);
body.append(summary);
}
const actions = document.createElement("div");
actions.className = "d-flex flex-wrap gap-2 mt-3";
if (route.selectable) { if (route.selectable) {
const actions = document.createElement("div");
actions.className = "d-flex gap-2 mt-3";
const start = document.createElement("button"); const start = document.createElement("button");
start.className = "btn btn-primary btn-sm"; start.className = "btn btn-primary btn-sm";
start.type = "button"; start.type = "button";
@@ -163,7 +286,12 @@ function renderSelectedProfile(): void {
start.disabled = active; start.disabled = active;
start.dataset.routeId = route.routeId; start.dataset.routeId = route.routeId;
start.addEventListener("click", () => { start.addEventListener("click", () => {
void startRoute(route.routeId).catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route Start failed")); selectedMonitoringKey = key;
clearRouteFeedback();
void startRoute(route.routeId).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "Le démarrage de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Start failed", { routeId: route.routeId });
});
}); });
actions.append(start); actions.append(start);
if (active && runtime !== null) { if (active && runtime !== null) {
@@ -171,13 +299,28 @@ function renderSelectedProfile(): void {
stop.className = "btn btn-outline-danger btn-sm"; stop.className = "btn btn-outline-danger btn-sm";
stop.type = "button"; stop.type = "button";
stop.textContent = "Stop"; stop.textContent = "Stop";
stop.disabled = runtime.state === "stopping"; stop.disabled = projectedState === "stopping";
stop.dataset.routeId = route.routeId; stop.dataset.routeId = route.routeId;
stop.addEventListener("click", () => { stop.addEventListener("click", () => {
void stopRoute(runtime).catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed")); clearRouteFeedback();
void stopRoute(runtime).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "L'arrêt de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed", { routeId: route.routeId });
});
}); });
actions.append(stop); actions.append(stop);
} }
}
if (monitoring) {
const supervise = document.createElement("button");
supervise.className = selectedMonitoringKey === key ? "btn btn-secondary btn-sm" : "btn btn-outline-secondary btn-sm";
supervise.type = "button";
supervise.textContent = "Supervision";
supervise.dataset.routeId = route.routeId;
supervise.addEventListener("click", () => selectRouteMonitoring(profile.profileId, route.routeId, route.family, "user"));
actions.append(supervise);
}
if (actions.childElementCount > 0) {
body.append(actions); body.append(actions);
} }
card.append(body); card.append(body);
@@ -185,14 +328,128 @@ function renderSelectedProfile(): void {
routeRoot.append(column); routeRoot.append(column);
} }
} }
renderMonitoringDetail();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", { frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
activeRouteCount: activeRuntimes.size, activeRouteCount: activeRuntimes.size,
monitoringCount: routeMonitoring.size,
profileId: profile.profileId, profileId: profile.profileId,
routeCount: profile.routes.length, routeCount: profile.routes.length,
selectableCount: profile.routes.filter(route => route.selectable).length, selectableCount: profile.routes.filter(route => route.selectable).length,
}); });
} }
function renderMonitoringDetail(): void {
const panel = document.querySelector<HTMLElement>("#routeMonitoringDetail");
if (!panel) {
return;
}
const monitoring = selectedMonitoringKey === null ? null : (routeMonitoring.get(selectedMonitoringKey) ?? null);
if (!monitoring) {
panel.hidden = true;
return;
}
panel.hidden = false;
text("#monitorRouteId", monitoring.routeId);
text("#monitorNetwork", monitoring.network);
text("#monitorCommitment", monitoring.commitment);
text("#monitorSequence", monitoring.sequence);
text("#monitorHealth", monitoring.health);
text("#monitorActivity", monitoring.activity);
text("#monitorSourceState", optionalText(monitoring.sourceState));
text("#monitorFault", monitoring.faultDomain === null || monitoring.faultCode === null ? "—" : `${monitoring.faultDomain}/${monitoring.faultCode}`);
const lifecycle = document.querySelector<HTMLElement>("#monitorLifecycle");
if (lifecycle) {
lifecycle.className = lifecycleBadgeClass(monitoring.state);
lifecycle.textContent = monitoring.state;
}
const health = document.querySelector<HTMLElement>("#monitorHealthBadge");
if (health) {
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = monitoring.health;
}
const activity = document.querySelector<HTMLElement>("#monitorActivityBadge");
if (activity) {
activity.className = "badge text-bg-light border text-dark";
activity.textContent = monitoring.activity;
}
const values: Record<string, string> = {
monitorAdmissionQueue: `${monitoring.admissionQueueDepth} / ${monitoring.admissionQueueCapacity}`,
monitorPersistence: `${monitoring.inFlightPersistence} / ${monitoring.persistenceConcurrency}`,
monitorAdmittedTotal: monitoring.admittedTotal,
monitorCanonicalizedTotal: monitoring.canonicalizedTotal,
monitorPersistedTotal: monitoring.persistedTotal,
monitorEntityInsertedTotal: monitoring.entityInsertedTotal,
monitorEntityAlreadyPresentTotal: monitoring.entityAlreadyPresentTotal,
monitorEntitySkippedPurgedTotal: monitoring.entitySkippedPurgedTotal,
monitorObservationInsertedTotal: monitoring.observationInsertedTotal,
monitorObservationAlreadyPresentTotal: monitoring.observationAlreadyPresentTotal,
monitorContentConflictTotal: monitoring.contentConflictTotal,
monitorStoreFailureTotal: monitoring.storeFailureTotal,
monitorSourceFailureTotal: monitoring.sourceFailureTotal,
monitorBackpressureWaitTotal: monitoring.backpressureWaitTotal,
monitorHydrationPending: monitoring.hydrationPending.toString(),
monitorProcessingFrontier: optionalText(monitoring.processingFrontierSlot),
monitorOldestPending: optionalText(monitoring.oldestPendingSlot),
monitorSourceCounts: `${monitoring.sourceActive} active / ${monitoring.sourceReconnecting} reconnecting / ${monitoring.sourceFailed} failed / ${monitoring.sourceTotal} total`,
monitorSourceReconnectTotal: monitoring.sourceReconnectTotal,
monitorSourceReplayAttemptTotal: monitoring.sourceReplayAttemptTotal,
monitorSourceContinuityGapTotal: monitoring.sourceContinuityGapTotal,
monitorContinuityPolicyObserved: yesNo(monitoring.continuityPolicyObserved),
monitorContinuityFrontier: optionalText(monitoring.continuityFrontierSlot),
monitorContinuityHasOpenGaps: yesNo(monitoring.continuityHasOpenGaps),
monitorFailedSourceLossesReconciled: yesNo(monitoring.failedSourceLossesReconciled),
monitorFutureTargetCoverage: yesNo(monitoring.futureTargetCoverage),
monitorOpenGapCount: monitoring.openGapCount.toString(),
monitorRepairingGapCount: monitoring.repairingGapCount.toString(),
monitorRepairedGapTotal: monitoring.repairedGapTotal,
monitorUnresolvedGapTotal: monitoring.unresolvedGapTotal,
monitorOldestOpenGap: optionalText(monitoring.oldestOpenGapStartSlot),
monitorReplayRepairTotal: monitoring.replayRepairTotal,
monitorRedundantCoverageRepairTotal: monitoring.redundantCoverageRepairTotal,
monitorHttpScanRepairTotal: monitoring.httpScanRepairTotal,
monitorRepairBlockFetchTotal: monitoring.repairBlockFetchTotal,
monitorRepairTransactionHydrationTotal: monitoring.repairTransactionHydrationTotal,
};
for (const [id, value] of Object.entries(values)) {
text(`#${id}`, value);
}
const gaps = document.querySelector<HTMLElement>("#monitorGapList");
if (gaps) {
gaps.replaceChildren();
if (monitoring.gaps.length === 0) {
const empty = document.createElement("div");
empty.className = "text-body-secondary small";
empty.textContent = "Aucun gap courant ou récent projeté.";
gaps.append(empty);
} else {
for (const gap of monitoring.gaps) {
const item = document.createElement("div");
item.className = "list-group-item";
const top = document.createElement("div");
top.className = "d-flex justify-content-between gap-3 flex-wrap";
const range = document.createElement("span");
range.className = "app-route-code";
range.textContent = `${gap.startSlot} ${gap.endSlot}`;
const badge = document.createElement("span");
badge.className = gap.state === "open" ? "badge text-bg-warning" : "badge text-bg-secondary";
badge.textContent = gap.state;
top.append(range, badge);
const meta = document.createElement("div");
meta.className = "small text-body-secondary mt-1";
meta.textContent = `gap ${gap.gapId} · ${gap.reason} · repair ${gap.lastMethod ?? "—"}`;
item.append(top, meta);
gaps.append(item);
}
}
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring detail rendered", {
gapCount: monitoring.gaps.length,
routeId: monitoring.routeId,
sequence: monitoring.sequence,
state: monitoring.state,
});
}
function selectedCommitment(): RawIngestCommitment { function selectedCommitment(): RawIngestCommitment {
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment"); const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
return selector?.value === "finalized" ? "finalized" : "confirmed"; return selector?.value === "finalized" ? "finalized" : "confirmed";
@@ -211,6 +468,11 @@ function renderMultiRouteRuntime(): void {
} }
const identities = active.map(runtime => `${runtime.routeId}/${runtime.commitment}`).join(", "); const identities = active.map(runtime => `${runtime.routeId}/${runtime.commitment}`).join(", ");
text("#routeRuntimeIdentity", identities.length === 0 ? "aucun Worker actif" : identities); text("#routeRuntimeIdentity", identities.length === 0 ? "aucun Worker actif" : identities);
const store = document.querySelector<HTMLElement>("#headerStore");
if (store) {
store.textContent = active.length === 0 ? "Store idle" : "Store shared";
store.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-success";
}
const profile = document.querySelector<HTMLSelectElement>("#routeProfile"); const profile = document.querySelector<HTMLSelectElement>("#routeProfile");
if (profile) { if (profile) {
profile.disabled = active.length > 0; profile.disabled = active.length > 0;
@@ -246,6 +508,8 @@ async function startRoute(routeId: RawIngestRouteId): Promise<void> {
} }
renderMultiRouteRuntime(); renderMultiRouteRuntime();
renderSelectedProfile(); renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} démarrée (${response.state}).`, "success");
await syncRouteMonitoring("start");
frontendInfo("main", "Raw Transaction Ingest Desk route Start acknowledged", { frontendInfo("main", "Raw Transaction Ingest Desk route Start acknowledged", {
activeRouteCount: activeRuntimes.size, activeRouteCount: activeRuntimes.size,
profileId: response.profileId, profileId: response.profileId,
@@ -273,6 +537,8 @@ async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
} }
renderMultiRouteRuntime(); renderMultiRouteRuntime();
renderSelectedProfile(); renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} arrêtée (${response.state}).`, "info");
await syncRouteMonitoring("stop");
frontendInfo("main", "Raw Transaction Ingest Desk targeted route Stop completed", { frontendInfo("main", "Raw Transaction Ingest Desk targeted route Stop completed", {
activeRouteCount: activeRuntimes.size, activeRouteCount: activeRuntimes.size,
profileId: response.profileId, profileId: response.profileId,
@@ -281,6 +547,63 @@ async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
}); });
} }
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
recordRouteMonitoring(status);
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
selectedMonitoringKey = runtimeKey(status.profileId, status.routeId);
}
renderMultiRouteRuntime();
renderSelectedProfile();
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
routeId: status.routeId,
sequence: status.sequence,
source,
state: status.state,
terminal: status.terminal,
});
}
async function syncRouteMonitoring(source: "startup" | "start" | "stop" | "user"): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization requested", { source });
const statuses = await invokeKsp<RawIngestRouteMonitoringDto[]>("main", "get_route_monitoring");
routeMonitoring.clear();
activeRuntimes.clear();
lastRuntimes.clear();
for (const status of statuses) {
recordRouteMonitoring(status);
}
if (selectedMonitoringKey !== null && !routeMonitoring.has(selectedMonitoringKey)) {
selectedMonitoringKey = null;
}
if (selectedMonitoringKey === null) {
const preferred = statuses.find(status => status.profileId === selectedProfileId) ?? statuses[0];
if (preferred) {
selectedMonitoringKey = runtimeKey(preferred.profileId, preferred.routeId);
}
}
renderMultiRouteRuntime();
renderSelectedProfile();
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization completed", {
activeRouteCount: activeRuntimes.size,
source,
statusCount: statuses.length,
});
}
async function bindRouteMonitoring(): Promise<void> {
await listen<RawIngestRouteMonitoringDto>("ksp-raw-ingest-route-status", event => {
applyRouteMonitoring(event.payload, "event");
});
document.querySelector<HTMLButtonElement>("#refreshRouteMonitoring")?.addEventListener("click", () => {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring refresh button clicked");
void syncRouteMonitoring("user").catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "La resynchronisation du monitoring a échoué."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route monitoring resynchronization failed");
});
});
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring listener and resynchronization control installed");
}
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void { function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
routeInventory = inventory; routeInventory = inventory;
const requestedProfile = selectedProfileId; const requestedProfile = selectedProfileId;
@@ -359,9 +682,22 @@ function bindRefreshControls(): void {
return; return;
} }
selectedProfileId = selector.value; selectedProfileId = selector.value;
frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", { profileId: selectedProfileId }); selectedMonitoringKey = null;
const selected = routeInventory?.profiles.find(profile => profile.profileId === selectedProfileId) ?? null;
frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", {
profileId: selectedProfileId,
routeCount: selected?.routes.length ?? 0,
selectionResolved: selected !== null,
});
renderSelectedProfile(); renderSelectedProfile();
}); });
document.querySelector<HTMLSelectElement>("#routeCommitment")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
frontendDebug("main", "Raw Transaction Ingest Desk commitment selection changed", { commitment: selectedCommitment() });
});
} }
async function initializeMain(): Promise<void> { async function initializeMain(): Promise<void> {
@@ -369,9 +705,10 @@ async function initializeMain(): Promise<void> {
bindFrontendInteractions(); bindFrontendInteractions();
bindNavigation(); bindNavigation();
bindRefreshControls(); bindRefreshControls();
await bindRouteMonitoring();
activateView("routes", "startup"); activateView("routes", "startup");
await refreshRouteInventory(); await refreshRouteInventory();
await refreshRuntimeStatus(); await Promise.all([refreshRuntimeStatus(), syncRouteMonitoring("startup")]);
frontendInfo("main", "Raw Transaction Ingest Desk multi-route shared-Store runtime frontend ready"); frontendInfo("main", "Raw Transaction Ingest Desk multi-route shared-Store runtime frontend ready");
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 7 // version: 8
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk. //! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
@@ -217,3 +217,39 @@ fn pre_010_runtime_monitoring_is_backend_owned_event_driven_and_resynchronizable
assert!(state.contains("route_monitoring(&self)")); assert!(state.contains("route_monitoring(&self)"));
assert!(state.contains("pre.010-runtime-monitoring")); assert!(state.contains("pre.010-runtime-monitoring"));
} }
#[test]
fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"listen<RawIngestRouteMonitoringDto>",
"ksp-raw-ingest-route-status",
"get_route_monitoring",
"routeMonitoring",
"syncRouteMonitoring",
"persistedTotal",
"sourceActive",
"openGapCount",
"repairTransactionHydrationTotal",
"route supervision selected",
"route monitoring resynchronization requested",
] {
assert!(main.contains(required), "missing pre.011 frontend monitoring marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in [
"id=\"refreshRouteMonitoring\"",
"id=\"routeMonitoringDetail\"",
"id=\"monitorPersistedTotal\"",
"id=\"monitorSourceCounts\"",
"id=\"monitorContinuityFrontier\"",
"id=\"monitorGapList\"",
"id=\"headerStore\"",
] {
assert!(html.contains(required), "missing pre.011 supervision UI marker {required}");
}
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
for required in [".app-route-monitoring-summary {", ".app-monitoring-detail {", ".app-monitoring-list dd", ".app-monitoring-gaps"] {
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 7 // version: 8
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold. //! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
@@ -160,3 +160,38 @@ fn pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_e
assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}"); assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}");
} }
} }
#[test]
fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"route supervision selected",
"route monitoring latest value applied",
"route monitoring resynchronization requested",
"route monitoring refresh button clicked",
"commitment selection changed",
"routeId",
"sequence",
"statusCount",
] {
assert!(main.contains(required), "missing pre.011 safe tracing marker {required}");
}
for forbidden in [
"endpointUrl",
"connectionUri",
"apiKey",
"sourceKey",
"workerId",
"x-token",
"rawTransaction",
"transactionPayload",
"canonicalBytes",
"wireBytes",
"transactionBytes",
"signature",
] {
assert!(!main.contains(forbidden), "pre.011 frontend exposes forbidden marker {forbidden}");
}
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 9 // version: 10
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction. //! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
@@ -167,3 +167,24 @@ fn pre_010_latest_value_monitoring_reuses_worker_snapshot_source_and_tauri_event
assert!(runtime.contains("monitoring_statuses")); assert!(runtime.contains("monitoring_statuses"));
assert!(runtime.contains("last_monitoring")); assert!(runtime.contains("last_monitoring"));
} }
#[test]
fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"bindRouteMonitoring",
"renderMonitoringDetail",
"syncRouteMonitoring(\"startup\")",
"syncRouteMonitoring(\"start\")",
"syncRouteMonitoring(\"stop\")",
] {
assert!(main.contains(required), "missing pre.011 frontend completeness marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["Supervision route", "Pipeline / persistence", "Sources / continuité / repair", "Gaps courants / récents"] {
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
}
}

174
deltas/0.3.15/pre.011.md Normal file
View File

@@ -0,0 +1,174 @@
<!-- file: deltas/0.3.15/pre.011.md -->
<!-- version: 1 -->
# Delta `0.3.15-pre.011`
## Base
```text
base archive : ksp-general-0.3.15-pre.010-fix.001.zip
base SHA-256 : 8a8f550cb5efbca5b8f7bd2f7ed0ed467a35b2e636b002fa2705bf302113bf5f
base version : 0.3.15-pre.10.fix.1
```
Le gate opérateur du 15 septembre 2026 confirme la base : audits Rust/Markdown propres, `cargo check --workspace`, Clippy strict, suites ciblées Raw Transaction Ingest Desk/Worker et workspace all-targets/all-features sans échec. Le canari `desktop_security` corrigé par `pre.010-fix.001` passe. Le lancement Tauri et les essais Mainnet confirment aussi Yellowstone et HTTP Block Polling productifs puis terminalement `Stopped` après Stop ciblé.
## Objectif
Finaliser le frontend fonctionnel de sélection/supervision au-dessus du contrat latest-value `pre.010`, sans modifier Config, Transport, Store, Worker, Common RAW ni les stratégies d'acquisition.
## Monitoring frontend
Le frontend écoute désormais :
```text
ksp-raw-ingest-route-status
```
et resynchronise explicitement via :
```text
get_route_monitoring
```
Une `Map` latest-value indexée par `profile_id + route_id` conserve les projections sûres. Chaque événement ou resync réconcilie également les `activeRuntimes` / `lastRuntimes` afin que :
```text
Starting / Running / Stopping -> route active
Stopped / Faulted -> route terminale
sélecteur réseau -> verrouillé tant qu'au moins un Worker est actif
header Store -> idle/shared selon le nombre de Workers actifs
```
Start et Stop déclenchent une resynchronisation après leur acknowledgement/terminal afin de couvrir une éventuelle perte d'événement UI sans introduire de polling métier.
## Supervision opérateur
Chaque route disposant d'un snapshot affiche dans sa carte :
```text
lifecycle
health
activity
persisted total
sources active / total
open gap count
```
Le contrôle `Supervision` ouvre un détail borné couvrant :
```text
identity logique : route / network / commitment / sequence
terminal fault domain/code sûrs
admission queue depth/capacity
persistence in-flight/concurrency
admitted / canonicalized / persisted
entity inserted / already present / purged skipped
observation inserted / already present
content/store/source failure counters
backpressure waits
hydration pending
processing frontier / oldest pending
source state / active / reconnecting / failed
reconnect / replay attempt / continuity-gap counters
continuity policy/frontier/open gaps/future coverage
failed-source loss reconciliation
open / repairing / repaired / unresolved gaps
bounded current/recent gap details
replay / redundant coverage / HTTP scan / block fetch / transaction hydration repair totals
```
Les slots, séquences et compteurs `u64` restent rendus directement sous forme de texte décimal. Le frontend ne les convertit jamais en `number`.
## Tracing TypeScript
La matrice prévue est matérialisée pour :
```text
clics génériques sans valeur de contrôle
navigation tabs
sélection profil/réseau avec counts sûrs
sélection commitment
sélection route de supervision avec route_id/family sûrs
Start / Stop avec route_id + lifecycle outcome
monitoring event avec route_id + sequence + state + terminal
refresh/resync avec source + status/active counts
IPC avec command id uniquement via invokeKsp
```
Le DTO monitoring complet n'est jamais sérialisé dans une trace. Aucun endpoint, URL, credential, source key, WorkerId, signature, transaction bytes, gap range ou payload RAW n'est journalisé.
## Canaris
Trois canaris de Desk sont ajoutés :
```text
pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision
pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material
pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth
```
Ils vérifient notamment que le module Rust de production du Desk reste exactement celui de `pre.010`.
## Version
```text
workspace.package.version : 0.3.15-pre.11
root Cargo header counter : 620
```
## Validation d'assemblage
Exécuté dans l'environnement d'assemblage :
```text
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
```
Un contrôle TypeScript strict auxiliaire a aussi compilé `main.ts`, `frontend_log.ts` et `invoke.ts` avec des déclarations temporaires reproduisant la surface des bindings `ts-rs` de `pre.010`; aucun diagnostic TypeScript n'a été produit. Ce contrôle ne remplace pas le build Tauri officiel.
Cargo/rustfmt ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Cargo de `pre.011` n'est déclaré PASS ici.
Le gate opérateur attendu reste :
```bash
cargo fmt --all
cargo fmt --all -- --check
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Aucun `npm run build` manuel n'est demandé ; le cycle frontend reste piloté par Tauri/Vite.
## Inventaire exact du delta
Ajout :
```text
deltas/0.3.15/pre.011.md
```
Modifications :
```text
Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/README.md
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html
crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
```
Suppressions : aucune.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md --> <!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Plan v0.3.15 — Raw Transaction Ingest Desk # Plan v0.3.15 — Raw Transaction Ingest Desk
@@ -817,13 +817,19 @@ Le relais Tauri suit le pattern KSP déjà utilisé par Backfill Desk : l'évén
Les compteurs, séquences et slots issus de `u64` sont sérialisés en texte décimal pour éviter toute perte de précision JavaScript. Les cardinalités bornées `usize` sont converties en `u32` avec échec fermé si elles deviennent non représentables. Le Worker ouvre uniquement cinq getters publics source-neutral déjà portés par son snapshot (`continuity_policy_observed`, `continuity_frontier_slot`, `continuity_has_open_gaps`, `failed_source_losses_reconciled`, `future_target_coverage`) ; aucune logique d'acquisition, retry, repair ou persistence n'est déplacée. Les compteurs, séquences et slots issus de `u64` sont sérialisés en texte décimal pour éviter toute perte de précision JavaScript. Les cardinalités bornées `usize` sont converties en `u32` avec échec fermé si elles deviennent non représentables. Le Worker ouvre uniquement cinq getters publics source-neutral déjà portés par son snapshot (`continuity_policy_observed`, `continuity_frontier_slot`, `continuity_has_open_gaps`, `failed_source_losses_reconciled`, `future_target_coverage`) ; aucune logique d'acquisition, retry, repair ou persistence n'est déplacée.
Le frontend détaillé ne consomme pas encore cette surface pour construire la vue de supervision complète : ce rendu et son tracing TypeScript restent `pre.011`. Cette surface est désormais le contrat backend figé consommé par le frontend de `pre.011`; aucune extension de l'acquisition n'est nécessaire pour construire la supervision opérateur.
Budget cible : une tranche observabilité. Budget cible : une tranche observabilité.
### `pre.011` — frontend fonctionnel et tracing TypeScript ### `pre.011` — frontend fonctionnel et tracing TypeScript
Finaliser l'UI de sélection/supervision et tracer les interactions frontend conformément aux règles Desk KSP, sans journaliser de valeurs sensibles ni dupliquer la logique Worker. **État : implémenté.** Le frontend consomme maintenant `ksp-raw-ingest-route-status` et `get_route_monitoring` sans polling métier ni seconde logique d'acquisition. Les latest values mettent à jour les cartes de routes, les états actifs/terminaux, le verrouillage du sélecteur réseau et l'indicateur du Store partagé.
Chaque route disposant d'un snapshot affiche un résumé source-neutral (`health`, `activity`, persisted, sources actives, gaps). Le contrôle `Supervision` ouvre un détail borné couvrant pipeline/admission/persistence, entity/observation outcomes, backpressure, hydration/pending/frontier, source lifecycle/reconnect/replay, continuité, gaps et compteurs de repair. Les slots/séquences/compteurs `u64` restent affichés comme texte décimal exact ; le frontend ne parse pas ces valeurs en `number`.
Le bouton `Resync monitoring` relit explicitement la latest value backend et réconcilie les cartes après événement manqué. Start et Stop déclenchent également une resynchronisation après leur acknowledgement/terminal. Le tracing TypeScript couvre clics, navigation, sélection profil/commitment/route, Start/Stop, événements monitoring, refresh/resync et IPC avec seulement IDs logiques sûrs, lifecycle codes, séquences et counts ; aucun endpoint, credential, source key, Worker handle, signature ou payload RAW n'est journalisé.
Aucun module Rust de production n'est ajouté dans cette tranche ; `pre.012` reste propriétaire des races Start/Stop, stale inventory, shutdown applicatif et hardening IPC hostile.
Budget cible : une tranche frontend. Budget cible : une tranche frontend.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md --> <!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
<!-- version: 19 --> <!-- version: 20 -->
# Validation v0.3.15 — Raw Transaction Ingest Desk # Validation v0.3.15 — Raw Transaction Ingest Desk
@@ -681,3 +681,51 @@ Le live Mainnet exécuté pendant ce gate confirme aussi que `yellowstone-hydrat
Le correctif ne modifie pas la projection runtime. Il sépare le canari en deux niveaux : les marqueurs physiques/secrets (`endpoint_url`, `connection_uri`, `api_key`, `source_key`, `worker_id`, `x-token`) restent interdits dans tout `route_monitoring.rs`, tandis que les marqueurs RAW (`raw_transaction`, `signature`, `payload`, `canonical_bytes`, `wire_bytes`, `transaction_bytes`) sont interdits dans la seule surface déclarative de `RawIngestRouteMonitoringDto`. Le test vérifie explicitement les bornes de cette déclaration. Il reste donc strict sur l'IPC sans confondre les noms normaux des crates/bindings avec des champs réellement projetés. Le correctif ne modifie pas la projection runtime. Il sépare le canari en deux niveaux : les marqueurs physiques/secrets (`endpoint_url`, `connection_uri`, `api_key`, `source_key`, `worker_id`, `x-token`) restent interdits dans tout `route_monitoring.rs`, tandis que les marqueurs RAW (`raw_transaction`, `signature`, `payload`, `canonical_bytes`, `wire_bytes`, `transaction_bytes`) sont interdits dans la seule surface déclarative de `RawIngestRouteMonitoringDto`. Le test vérifie explicitement les bornes de cette déclaration. Il reste donc strict sur l'IPC sans confondre les noms normaux des crates/bindings avec des champs réellement projetés.
### `pre.011` — frontend fonctionnel et tracing TypeScript
Base : `0.3.15-pre.010-fix.001`, validée par le gate opérateur du 15 septembre 2026. Le correctif de canari `desktop_security` est confirmé propre ; les audits Rust/Markdown, `cargo check --workspace`, Clippy strict, les suites Raw Transaction Ingest Desk/Worker et le workspace all-targets/all-features sont passés avant le lancement Tauri. Les smokes opérateur Mainnet montrent également Yellowstone et HTTP Block Polling productifs puis terminalement `Stopped` après Stop ciblé.
Périmètre `pre.011` : frontend uniquement au-dessus du contrat latest-value `pre.010`.
```text
écoute Tauri ksp-raw-ingest-route-status
resynchronisation get_route_monitoring
Map frontend latest-value par profile_id + route_id
réconciliation des active/last runtimes depuis le snapshot Worker projeté
résumé route : lifecycle / health / activity / persisted / sources / gaps
détail supervision : admission / persistence / outcomes / backpressure / hydration / frontier
sources : active / reconnecting / failed / reconnect / replay / continuity-gap counters
continuité : frontier / open gaps / failed-source reconciliation / future coverage
gaps bornés : range / state / reason / latest repair method
repair totals : replay / redundant coverage / HTTP scan / block fetch / transaction hydration
resync explicite après Start, Stop et action opérateur
Store header dérivé du nombre de Workers actifs, sans exposer de handle Store
```
Le frontend ne transforme jamais les compteurs, slots ou séquences `u64` en `number`; les chaînes décimales `pre.010` sont rendues directement. Les cardinalités `u32` restent les seules valeurs numériques de présentation.
Tracing TypeScript ajouté/fermé :
```text
clics génériques sans valeur de contrôle
navigation tabs
sélection profil logique avec counts sûrs
sélection commitment
sélection route de supervision avec route_id/family sûrs
Start/Stop avec route_id/lifecycle
latest-value event avec route_id/sequence/state/terminal
refresh/resync monitoring avec source/counts
IPC via invokeKsp avec command id seulement
```
Les traces ne sérialisent jamais le DTO monitoring complet et ne journalisent aucun gap slot/range, endpoint, URL, credential, provider secret, source key, WorkerId, signature, transaction bytes ou payload RAW.
Canaris ajoutés :
```text
pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision
pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material
pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth
```
Aucune modification Config, Transport, Store, Worker ou Common RAW n'est introduite par `pre.011`. Le module Rust de production du Desk reste exactement celui de `pre.010`; la tranche suivante `pre.012` reste dédiée aux races/shutdown/sécurité IPC.