0.3.15-pre.011-fix.001

This commit is contained in:
2026-09-16 09:38:30 +02:00
parent a248be04ca
commit 2f587e22be
10 changed files with 266 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# `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.
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.
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 coalescées côté backend à une cadence bornée de `500 ms` maximum par route, é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. Les snapshots steady-state patchent les champs de carte en place ; la reconstruction des contrôles Start/Stop est réservée aux changements de lifecycle, afin de préserver la réactivité des interactions.
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 -->
<!-- version: 11 -->
<!-- version: 12 -->
# 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.
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.
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. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. 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.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
// version: 7
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -29,6 +29,7 @@ const viewTitles: Record<ViewId, string> = {
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
const routeCards = new Map<string, HTMLElement>();
let routeInventory: RawIngestRouteInventoryDto | null = null;
let selectedProfileId: string | null = null;
let selectedMonitoringKey: string | null = null;
@@ -215,6 +216,7 @@ function renderSelectedProfile(): void {
text("#headerProfile", profile.profileId);
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
if (routeRoot) {
routeCards.clear();
routeRoot.replaceChildren();
for (const route of profile.routes) {
const key = runtimeKey(profile.profileId, route.routeId);
@@ -246,6 +248,7 @@ function renderSelectedProfile(): void {
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
const state = document.createElement("span");
state.className = lifecycleBadgeClass(projectedState);
state.dataset.monitoringField = "state";
state.textContent = projectedState;
const network = document.createElement("span");
network.className = "badge text-bg-light border text-dark";
@@ -260,18 +263,23 @@ function renderSelectedProfile(): void {
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.dataset.monitoringField = "health";
health.textContent = `health: ${monitoring.health}`;
const activity = document.createElement("span");
activity.className = "badge text-bg-light border text-dark";
activity.dataset.monitoringField = "activity";
activity.textContent = `activity: ${monitoring.activity}`;
const persisted = document.createElement("span");
persisted.className = "badge text-bg-light border text-dark";
persisted.dataset.monitoringField = "persisted";
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
const sources = document.createElement("span");
sources.className = "badge text-bg-light border text-dark";
sources.dataset.monitoringField = "sources";
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.dataset.monitoringField = "gaps";
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
summary.append(health, activity, persisted, sources, gaps);
body.append(summary);
@@ -324,6 +332,7 @@ function renderSelectedProfile(): void {
body.append(actions);
}
card.append(body);
routeCards.set(key, card);
column.append(card);
routeRoot.append(column);
}
@@ -450,6 +459,34 @@ function renderMonitoringDetail(): void {
});
}
function renderRouteMonitoringCard(status: RawIngestRouteMonitoringDto): boolean {
const key = runtimeKey(status.profileId, status.routeId);
const card = routeCards.get(key);
if (!card) {
return false;
}
const field = (name: string): HTMLElement | null => card.querySelector<HTMLElement>(`[data-monitoring-field="${name}"]`);
const state = field("state");
const health = field("health");
const activity = field("activity");
const persisted = field("persisted");
const sources = field("sources");
const gaps = field("gaps");
if (!state || !health || !activity || !persisted || !sources || !gaps) {
return false;
}
state.className = lifecycleBadgeClass(status.state);
state.textContent = status.state;
health.className = status.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = `health: ${status.health}`;
activity.textContent = `activity: ${status.activity}`;
persisted.textContent = `persisted: ${status.persistedTotal}`;
sources.textContent = `sources: ${status.sourceActive}/${status.sourceTotal}`;
gaps.className = status.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.textContent = `gaps: ${status.openGapCount}`;
return true;
}
function selectedCommitment(): RawIngestCommitment {
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
return selector?.value === "finalized" ? "finalized" : "confirmed";
@@ -548,13 +585,21 @@ async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
}
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
const key = runtimeKey(status.profileId, status.routeId);
const previous = routeMonitoring.get(key) ?? null;
recordRouteMonitoring(status);
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
selectedMonitoringKey = runtimeKey(status.profileId, status.routeId);
selectedMonitoringKey = key;
}
renderMultiRouteRuntime();
renderSelectedProfile();
const lifecycleChanged = previous === null || previous.state !== status.state;
if (lifecycleChanged || !renderRouteMonitoringCard(status)) {
renderSelectedProfile();
} else if (selectedMonitoringKey === key) {
renderMonitoringDetail();
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
lifecycleChanged,
routeId: status.routeId,
sequence: status.sequence,
source,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 7
// version: 8
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
@@ -186,6 +186,8 @@ async fn start_route(
return std::result::Result::Ok(acknowledgement);
}
const ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500;
async fn monitor_route_status(
app: tauri::AppHandle,
runtime: crate::RawIngestRouteRuntimeDto,
@@ -223,6 +225,10 @@ async fn monitor_route_status(
}
let observed = snapshot.worker_snapshot().sequence();
snapshot = source.wait_for_change(observed).await;
if !snapshot.worker_snapshot().state().is_terminal() {
tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await;
snapshot = source.current();
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 8
// version: 9
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
@@ -253,3 +253,24 @@ fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supe
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
}
}
#[test]
fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_route_controls() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500",
"tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await",
"snapshot = source.current()",
] {
assert!(tauri.contains(required), "missing pre.011-fix.001 bounded relay marker {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"const routeCards = new Map<string, HTMLElement>()",
"renderRouteMonitoringCard",
"const lifecycleChanged = previous === null || previous.state !== status.state",
"if (lifecycleChanged || !renderRouteMonitoringCard(status))",
] {
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 10
// version: 11
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
@@ -188,3 +188,15 @@ fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth(
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
}
}
#[test]
fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("source.wait_for_change(observed).await"));
assert!(tauri.contains("ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500"));
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("renderRouteMonitoringCard"));
assert!(main.contains("renderSelectedProfile"));
}