// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts // version: 5 import "bootstrap"; import ResizeObserver from "resize-observer-polyfill"; import "simplebar"; 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 { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts"; import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.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 { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log"; import { invokeKsp } from "./invoke"; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; installFrontendConsoleBridge("main"); type ViewId = "routes" | "diagnostics"; const viewTitles: Record = { routes: "Routes", diagnostics: "Diagnostics", }; let activeRuntime: RawIngestRouteRuntimeDto | null = null; let lastRuntime: RawIngestRouteRuntimeDto | null = null; let routeInventory: RawIngestRouteInventoryDto | null = null; let selectedProfileId: string | null = null; function isViewId(value: string | undefined): value is ViewId { return value === "routes" || value === "diagnostics"; } function activateView(viewId: ViewId, source: "startup" | "user"): void { document.querySelectorAll("[data-view-panel]").forEach(panel => { panel.hidden = panel.dataset.viewPanel !== viewId; }); document.querySelectorAll("[data-view]").forEach(button => { const active = button.dataset.view === viewId; button.classList.toggle("active", active); button.setAttribute("aria-current", active ? "page" : "false"); }); const header = document.querySelector("#headerViewTitle"); if (header) { header.textContent = viewTitles[viewId]; } document.title = `Raw Transaction Ingest Desk — ${viewTitles[viewId]}`; frontendTrace("main", "Raw Transaction Ingest Desk view DOM updated", { viewId, source }); } function bindFrontendInteractions(): void { document.addEventListener( "click", event => { const source = event.target; if (!(source instanceof Element)) { return; } const control = source.closest('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]'); if (!control) { return; } frontendTrace("main", "Raw Transaction Ingest Desk frontend control clicked", { controlId: control.id || null, role: control.getAttribute("role"), tagName: control.tagName.toLowerCase(), viewId: control.dataset.view ?? null, }); }, true, ); } function bindNavigation(): void { document.querySelectorAll("[data-view]").forEach(button => { button.addEventListener("click", () => { const viewId = button.dataset.view; frontendDebug("main", "Raw Transaction Ingest Desk navigation tab clicked", { viewId: viewId ?? "unknown" }); if (isViewId(viewId)) { activateView(viewId, "user"); } }); }); } function reasonLabel(reason: string | null): string { const labels: Record = { profile_unresolved: "profil Config non résolu", network_mismatch: "réseau Transport/Store incohérent", missing_http_get_transaction: "HTTP getTransaction indisponible", missing_http_block_scan: "HTTP block scan incomplet", missing_ws_logs_capability: "capability WS Logs absente", missing_ws_block_capability: "capability WS Block absente", missing_helius_transaction_capability: "capability Helius Transaction absente", missing_yellowstone_grpc: "Yellowstone gRPC absent", missing_required_secret: "secret Config requis non résolu", }; if (reason === null) { return "composable depuis Config"; } return labels[reason] ?? reason; } function renderSelectedProfile(): void { if (routeInventory === null) { return; } const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0]; if (!profile) { return; } selectedProfileId = profile.profileId; text("#routeNetwork", profile.network ?? "non résolu"); text("#routeSelectedProfile", profile.profileId); text("#headerProfile", profile.profileId); const routeRoot = document.querySelector("#routeFoundation"); if (routeRoot) { routeRoot.replaceChildren(); for (const route of profile.routes) { const column = document.createElement("div"); column.className = "col-12 col-xl-6 app-route-column"; const card = document.createElement("div"); card.className = "card h-100 shadow-sm app-route-card"; const body = document.createElement("div"); body.className = "card-body"; const title = document.createElement("h2"); title.className = "h6 mb-2"; title.textContent = route.label; const code = document.createElement("div"); code.className = "app-route-code text-body-secondary small"; code.textContent = route.routeId; const meta = document.createElement("div"); meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta"; const state = document.createElement("span"); state.className = route.selectable ? "badge text-bg-success" : "badge text-bg-secondary"; state.textContent = route.state; const network = document.createElement("span"); network.className = "badge text-bg-light border text-dark"; network.textContent = route.network ?? "network unresolved"; const reason = document.createElement("span"); reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning"; reason.textContent = reasonLabel(route.reason); meta.append(state, network, reason); body.append(title, code, meta); if (route.selectable) { const actions = document.createElement("div"); actions.className = "mt-3"; const start = document.createElement("button"); start.className = "btn btn-primary btn-sm"; start.type = "button"; start.textContent = "Start"; start.disabled = activeRuntime !== null; start.dataset.routeId = route.routeId; start.addEventListener("click", () => { void startRoute(route.routeId).catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route Start failed")); }); actions.append(start); body.append(actions); } card.append(body); column.append(card); routeRoot.append(column); } } frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", { profileId: profile.profileId, routeCount: profile.routes.length, selectableCount: profile.routes.filter(route => route.selectable).length, }); } function selectedCommitment(): RawIngestCommitment { const selector = document.querySelector("#routeCommitment"); return selector?.value === "finalized" ? "finalized" : "confirmed"; } function runtimeIsActive(runtime: RawIngestRouteRuntimeDto): boolean { return runtime.state === "starting" || runtime.state === "running" || runtime.state === "stopping"; } function renderMonoRouteRuntime(): void { const projected = activeRuntime ?? lastRuntime; const state = document.querySelector("#routeRuntimeState"); if (state) { state.textContent = projected?.state ?? "idle"; state.className = projected?.state === "faulted" ? "badge text-bg-danger" : activeRuntime ? "badge text-bg-primary" : "badge text-bg-secondary"; } text( "#routeRuntimeIdentity", projected ? `${projected.profileId} / ${projected.routeId} / ${projected.commitment}` : "aucun Worker actif", ); const stop = document.querySelector("#stopRoute"); if (stop) { stop.disabled = activeRuntime === null || activeRuntime.state === "stopping"; } const profile = document.querySelector("#routeProfile"); if (profile) { profile.disabled = activeRuntime !== null; } const commitment = document.querySelector("#routeCommitment"); if (commitment) { commitment.disabled = activeRuntime !== null; } } async function startRoute(routeId: RawIngestRouteId): Promise { if (routeInventory === null || selectedProfileId === null || activeRuntime !== null) { return; } const request: RawIngestRouteStartRequestDto = { commitment: selectedCommitment(), inventoryGeneration: routeInventory.generation, profileId: selectedProfileId, routeId, }; frontendDebug("main", "Raw Transaction Ingest Desk mono-route Start requested", { commitment: request.commitment, profileId: request.profileId, routeId: request.routeId, }); const response = await invokeKsp("main", "start_route", { request }); lastRuntime = response; activeRuntime = runtimeIsActive(response) ? response : null; renderMonoRouteRuntime(); renderSelectedProfile(); frontendInfo("main", "Raw Transaction Ingest Desk mono-route Start acknowledged", { profileId: response.profileId, routeId: response.routeId, state: response.state, }); } async function stopRoute(): Promise { if (activeRuntime === null) { return; } frontendDebug("main", "Raw Transaction Ingest Desk mono-route Stop requested", { profileId: activeRuntime.profileId, routeId: activeRuntime.routeId, }); const response = await invokeKsp("main", "stop_route"); lastRuntime = response; activeRuntime = runtimeIsActive(response) ? response : null; renderMonoRouteRuntime(); renderSelectedProfile(); frontendInfo("main", "Raw Transaction Ingest Desk mono-route Stop completed", { profileId: response.profileId, routeId: response.routeId, state: response.state, }); } function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void { routeInventory = inventory; const requestedProfile = selectedProfileId; const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile); selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile; const selector = document.querySelector("#routeProfile"); if (selector) { selector.replaceChildren(); for (const profile of inventory.profiles) { const option = document.createElement("option"); option.value = profile.profileId; option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId; option.selected = profile.profileId === selectedProfileId; selector.append(option); } } text("#routeInventoryGeneration", inventory.generation.toString()); text("#runtimeInventoryGeneration", inventory.generation.toString()); text("#runtimeInventoryDefaultProfile", inventory.defaultProfile); renderSelectedProfile(); renderMonoRouteRuntime(); frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", { generation: inventory.generation, profileCount: inventory.profiles.length, }); } function text(selector: string, value: string): void { const element = document.querySelector(selector); if (element) { element.textContent = value; } } function renderRuntimeStatus(status: ShellStatusDto): void { text("#runtimeVersion", status.applicationVersion); text("#runtimeShellPhase", status.shellPhase); text("#runtimeConfigDocuments", status.configDocumentCount.toString()); text("#runtimeCompositeProfile", status.activeCompositeProfile ?? "fallback/unresolved"); text("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback"); text("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non"); text("#headerPhase", status.shellPhase); const diagnostic = document.querySelector("#runtimeDiagnostic"); if (diagnostic) { diagnostic.hidden = status.startupDiagnostic === null; diagnostic.textContent = status.startupDiagnostic ? `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}` : ""; } frontendTrace("main", "Raw Transaction Ingest Desk runtime status rendered", { fallbackLoggingActive: status.fallbackLoggingActive, hasStartupDiagnostic: status.startupDiagnostic !== null, }); } async function refreshRouteInventory(): Promise { frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested"); const inventory = await invokeKsp("main", "get_route_inventory"); renderRouteInventory(inventory); } async function refreshRuntimeStatus(): Promise { frontendDebug("main", "Raw Transaction Ingest Desk runtime status refresh requested"); const status = await invokeKsp("main", "get_runtime_status"); renderRuntimeStatus(status); } function bindRefreshControls(): void { document.querySelector("#refreshRouteInventory")?.addEventListener("click", () => { void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed")); }); document.querySelector("#refreshRuntimeStatus")?.addEventListener("click", () => { void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed")); }); document.querySelector("#stopRoute")?.addEventListener("click", () => { void stopRoute().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed")); }); document.querySelector("#routeProfile")?.addEventListener("change", event => { const selector = event.currentTarget; if (!(selector instanceof HTMLSelectElement)) { return; } selectedProfileId = selector.value; frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", { profileId: selectedProfileId }); renderSelectedProfile(); }); } async function initializeMain(): Promise { frontendInfo("main", "Raw Transaction Ingest Desk main frontend loaded"); bindFrontendInteractions(); bindNavigation(); bindRefreshControls(); activateView("routes", "startup"); await refreshRouteInventory(); await refreshRuntimeStatus(); frontendInfo("main", "Raw Transaction Ingest Desk mono-route runtime frontend ready"); } void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));