// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts // version: 8 import "bootstrap"; import ResizeObserver from "resize-observer-polyfill"; import "simplebar"; import { getCurrentWindow } from "@tauri-apps/api/window"; import type { MarketPriceRuntimeStatusDto } from "./bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts"; import type { MarketPriceProviderRowDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts"; import type { MarketPriceRefreshManyRequestDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts"; import type { MarketPriceRefreshResultDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.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 = "prices" | "diagnostics"; type AvailabilityPresentation = { badgeClass: string; iconClass: string; label: string; }; const viewTitles: Record = { prices: "Prices", diagnostics: "Diagnostics", }; const availabilityPresentations: Record = { authentication_unavailable: { badgeClass: "text-bg-danger", iconClass: "fa-key", label: "Authentication unavailable" }, cooling_down: { badgeClass: "text-bg-warning", iconClass: "fa-clock", label: "Cooling down" }, disabled: { badgeClass: "text-bg-secondary", iconClass: "fa-ban", label: "Disabled" }, misconfigured: { badgeClass: "text-bg-danger", iconClass: "fa-triangle-exclamation", label: "Misconfigured" }, quota_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-gauge-high", label: "Quota unavailable" }, ready: { badgeClass: "text-bg-success", iconClass: "fa-circle-check", label: "Ready" }, temporarily_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-cloud-arrow-down", label: "Temporarily unavailable" }, unknown: { badgeClass: "text-bg-secondary", iconClass: "fa-circle-question", label: "Unknown" }, }; const authLabels: Record = { none: "Keyless", optional_api_key: "Optional API key", required_api_key: "API key required", unknown: "Unknown", }; const semanticsLabels: Record = { aggregated_market: "Aggregated market", dex_pair_usd: "DEX pair USD", exchange_last_trade: "Exchange last trade", solana_heuristic: "Solana heuristic", solana_spot: "Solana spot", unknown: "Unknown", }; const jsDateMaxUnixMillis = 8_640_000_000_000_000n; let currentMarketPriceRows: MarketPriceProviderRowDto[] = []; const selectedProviderIds = new Set(); function isViewId(value: string | undefined): value is ViewId { return value === "prices" || 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 => { button.classList.toggle("active", button.dataset.view === viewId); }); const header = document.querySelector("#headerViewTitle"); if (header) { header.textContent = viewTitles[viewId]; } document.title = `SOL Prices Desk — ${viewTitles[viewId]}`; if (source !== "startup") { frontendDebug("main", "SOL Prices Desk view activated", { viewId, source }); } } function bindNavigation(): void { document.querySelectorAll("[data-view]").forEach(button => { button.addEventListener("click", () => { const viewId = button.dataset.view; frontendDebug("main", "SOL Prices Desk navigation control clicked", { viewId: viewId ?? "unknown" }); if (isViewId(viewId)) { activateView(viewId, "user"); } }); }); frontendTrace("main", "SOL Prices Desk navigation handlers installed"); } function clearPricesDiagnostic(): void { const diagnostic = document.querySelector("#pricesDiagnostic"); if (diagnostic) { diagnostic.hidden = true; diagnostic.textContent = ""; } } function showPricesDiagnostic(message: string): void { const diagnostic = document.querySelector("#pricesDiagnostic"); if (diagnostic) { diagnostic.hidden = false; diagnostic.textContent = message; } } function selectedProviderIdsInRowOrder(): string[] { return currentMarketPriceRows.filter(provider => selectedProviderIds.has(provider.providerId)).map(provider => provider.providerId); } function synchronizeSelectedProviders(): void { const currentIds = new Set(currentMarketPriceRows.map(provider => provider.providerId)); for (const providerId of selectedProviderIds) { if (!currentIds.has(providerId)) { selectedProviderIds.delete(providerId); } } } function updateSummary(): void { const summaryValues: Record = { pricesSummaryProviders: currentMarketPriceRows.length, pricesSummaryReady: currentMarketPriceRows.filter(provider => provider.availability === "ready" && !provider.loading).length, pricesSummaryObserved: currentMarketPriceRows.filter(provider => provider.refreshed).length, pricesSummaryAttention: currentMarketPriceRows.filter(provider => provider.availability !== "ready").length, pricesSummaryLoading: currentMarketPriceRows.filter(provider => provider.loading).length, }; for (const [elementId, value] of Object.entries(summaryValues)) { const element = document.querySelector(`#${elementId}`); if (element) { element.textContent = value.toString(); } } } function synchronizeBatchControls(): void { const selectedCount = selectedProviderIdsInRowOrder().length; const providerCount = currentMarketPriceRows.length; const anyLoading = currentMarketPriceRows.some(provider => provider.loading); const selectedButton = document.querySelector("#refreshSelectedButton"); const allButton = document.querySelector("#refreshAllButton"); const clearButton = document.querySelector("#clearSelectionButton"); const selectAll = document.querySelector("#selectAllProviders"); const selectedCountLabel = document.querySelector("#selectedProviderCount"); if (selectedButton) { selectedButton.disabled = selectedCount === 0 || anyLoading; } if (allButton) { allButton.disabled = providerCount === 0 || anyLoading; } if (clearButton) { clearButton.disabled = selectedCount === 0 || anyLoading; } if (selectAll) { selectAll.disabled = providerCount === 0 || anyLoading; selectAll.checked = providerCount !== 0 && selectedCount === providerCount; selectAll.indeterminate = selectedCount > 0 && selectedCount < providerCount; } if (selectedCountLabel) { selectedCountLabel.textContent = `${selectedCount} selected`; } updateSummary(); } function markProvidersLoading(providerIds: string[]): void { const loadingIds = new Set(providerIds); currentMarketPriceRows = currentMarketPriceRows.map(provider => { return loadingIds.has(provider.providerId) ? { ...provider, loading: true } : provider; }); renderMarketPriceRows(currentMarketPriceRows); } function applyRefreshResult(result: MarketPriceRefreshResultDto): void { const refreshedRows = new Map(); for (const provider of result.rows) { refreshedRows.set(provider.providerId, provider); } currentMarketPriceRows = currentMarketPriceRows.map(provider => refreshedRows.get(provider.providerId) ?? provider); renderMarketPriceRows(currentMarketPriceRows); } async function recoverMarketPriceRows(message: string): Promise { showPricesDiagnostic(message); try { await loadMarketPrices(); } catch { frontendWarn("main", "SOL Prices Desk market-price refresh recovery reload failed"); } } async function refreshMarketPrice(providerId: string): Promise { frontendDebug("main", "SOL Prices Desk market-price refresh control clicked", { providerId }); clearPricesDiagnostic(); markProvidersLoading([providerId]); try { const refreshed = await invokeKsp("main", "refresh_market_price", { providerId }); currentMarketPriceRows = currentMarketPriceRows.map(provider => { return provider.providerId === refreshed.providerId ? refreshed : provider; }); renderMarketPriceRows(currentMarketPriceRows); frontendTrace("main", "SOL Prices Desk market-price refresh row applied", { providerId }); } catch { frontendWarn("main", "SOL Prices Desk market-price refresh failed", { providerId }); await recoverMarketPriceRows("Le provider sélectionné n'a pas pu être rafraîchi. L'état précédent est conservé lorsqu'il reste disponible."); } } async function refreshSelectedMarketPrices(): Promise { const providerIds = selectedProviderIdsInRowOrder(); frontendDebug("main", "SOL Prices Desk selected market-price refresh control clicked", { selectedCount: providerIds.length }); if (providerIds.length === 0) { return; } clearPricesDiagnostic(); markProvidersLoading(providerIds); const request: MarketPriceRefreshManyRequestDto = { providerIds }; try { const result = await invokeKsp("main", "refresh_market_prices", { request }); applyRefreshResult(result); frontendTrace("main", "SOL Prices Desk selected market-price refresh result applied", { requestedCount: result.requestedCount, refreshedCount: result.refreshedCount, }); } catch { frontendWarn("main", "SOL Prices Desk selected market-price refresh failed", { selectedCount: providerIds.length }); await recoverMarketPriceRows("La sélection n'a pas pu être rafraîchie. Les observations précédentes restent visibles avec leur état réel."); } } async function refreshAllMarketPrices(): Promise { frontendDebug("main", "SOL Prices Desk global market-price refresh control clicked", { providerCount: currentMarketPriceRows.length }); if (currentMarketPriceRows.length === 0) { return; } clearPricesDiagnostic(); markProvidersLoading(currentMarketPriceRows.map(provider => provider.providerId)); try { const result = await invokeKsp("main", "refresh_all_market_prices"); applyRefreshResult(result); frontendTrace("main", "SOL Prices Desk global market-price refresh result applied", { requestedCount: result.requestedCount, refreshedCount: result.refreshedCount, }); } catch { frontendWarn("main", "SOL Prices Desk global market-price refresh failed", { providerCount: currentMarketPriceRows.length }); await recoverMarketPriceRows("Le refresh global n'a pas pu être terminé. Les observations précédentes restent visibles avec leur état réel."); } } function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); cell.className = "app-market-selection-column"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "form-check-input"; checkbox.checked = selectedProviderIds.has(provider.providerId); checkbox.disabled = provider.loading; checkbox.setAttribute("aria-label", `Select ${provider.displayName}`); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedProviderIds.add(provider.providerId); } else { selectedProviderIds.delete(provider.providerId); } frontendDebug("main", "SOL Prices Desk market-price selection control changed", { providerId: provider.providerId, selected: checkbox.checked, }); synchronizeBatchControls(); }); cell.append(checkbox); row.append(cell); } function appendProviderCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); const name = document.createElement("div"); name.className = "app-market-provider-name"; name.textContent = provider.displayName; const code = document.createElement("div"); code.className = "app-market-code"; code.textContent = provider.providerId; cell.append(name, code); row.append(cell); } function appendBadgeCell(row: HTMLTableRowElement, label: string, badgeClass = "text-bg-light", className?: string): void { const cell = document.createElement("td"); if (className) { cell.className = className; } const badge = document.createElement("span"); badge.className = `badge ${badgeClass}`; badge.textContent = label; cell.append(badge); row.append(cell); } function availabilityPresentation(provider: MarketPriceProviderRowDto): AvailabilityPresentation { if (provider.loading) { return { badgeClass: "text-bg-info", iconClass: "fa-spinner fa-spin", label: "Refreshing" }; } return availabilityPresentations[provider.availability] ?? availabilityPresentations.unknown; } function appendAvailabilityCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); const presentation = availabilityPresentation(provider); const badge = document.createElement("span"); badge.className = `badge ${presentation.badgeClass}`; const icon = document.createElement("i"); icon.className = `fa-solid ${presentation.iconClass} me-1`; icon.setAttribute("aria-hidden", "true"); badge.append(icon, document.createTextNode(presentation.label)); cell.append(badge); row.append(cell); } function appendPriceCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); if (provider.price === null) { const empty = document.createElement("span"); empty.className = "text-body-secondary"; empty.textContent = "No observation"; cell.append(empty); } else { const price = document.createElement("span"); price.className = "app-market-price"; price.textContent = provider.price; const currency = document.createElement("small"); currency.className = "text-body-secondary ms-1"; currency.textContent = "USD"; cell.append(price, currency); } row.append(cell); } function formatUnixMillis(value: string): { exact: string; iso: string; local: string } | null { try { const exact = BigInt(value); if (exact < -jsDateMaxUnixMillis || exact > jsDateMaxUnixMillis) { return null; } const date = new Date(Number(exact)); if (Number.isNaN(date.getTime())) { return null; } return { exact: value, iso: date.toISOString(), local: date.toLocaleString("fr-CH", { dateStyle: "short", timeStyle: "medium", hour12: false }), }; } catch { return null; } } function appendTimestampValue(container: HTMLElement, label: string, value: string | null, emptyLabel: string): void { const line = document.createElement("div"); line.className = "app-market-time-line"; const prefix = document.createElement("span"); prefix.className = "app-market-time-label"; prefix.textContent = label; const valueContainer = document.createElement("span"); valueContainer.className = "app-market-time-value"; if (value === null) { valueContainer.classList.add("text-body-secondary"); valueContainer.textContent = emptyLabel; } else { const formatted = formatUnixMillis(value); if (formatted === null) { const exact = document.createElement("span"); exact.className = "app-market-time-exact"; exact.textContent = `${value} ms`; valueContainer.append(exact); } else { const time = document.createElement("time"); time.dateTime = formatted.iso; time.textContent = formatted.local; time.title = formatted.iso; const exact = document.createElement("span"); exact.className = "app-market-time-exact d-block"; exact.textContent = `${formatted.exact} ms`; valueContainer.append(time, exact); } } line.append(prefix, valueContainer); container.append(line); } function appendObservationTimeCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); cell.className = "app-market-time-column"; const time = document.createElement("div"); time.className = "app-market-time"; appendTimestampValue(time, "Provider", provider.providerTimestampUnixMillis, "Not supplied"); appendTimestampValue(time, "KSP", provider.receivedAtUnixMillis, "—"); cell.append(time); row.append(cell); } function appendRetryCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); cell.className = "app-market-time-column"; const time = document.createElement("div"); time.className = "app-market-time"; appendTimestampValue(time, "Retry", provider.retryAtUnixMillis, "—"); cell.append(time); row.append(cell); } function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void { const cell = document.createElement("td"); cell.className = "text-end"; const button = document.createElement("button"); button.type = "button"; button.className = "btn btn-sm btn-outline-primary text-nowrap"; button.disabled = provider.loading; button.dataset.providerId = provider.providerId; button.setAttribute("aria-label", `Refresh ${provider.displayName}`); button.innerHTML = provider.loading ? 'Refreshing…' : 'Refresh'; button.addEventListener("click", () => { void refreshMarketPrice(provider.providerId); }); cell.append(button); row.append(cell); } function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void { const loading = document.querySelector("#pricesLoading"); const empty = document.querySelector("#pricesEmpty"); const tableContainer = document.querySelector("#pricesTableContainer"); const tableBody = document.querySelector("#marketPriceRows"); if (loading) { loading.hidden = true; } if (!tableBody || !tableContainer || !empty) { frontendWarn("main", "SOL Prices Desk market-price table elements are missing"); return; } currentMarketPriceRows = rows; synchronizeSelectedProviders(); tableBody.replaceChildren(); empty.hidden = rows.length !== 0; tableContainer.hidden = rows.length === 0; for (const provider of rows) { const row = document.createElement("tr"); row.dataset.providerId = provider.providerId; row.dataset.availability = provider.availability; row.classList.toggle("app-market-row-loading", provider.loading); appendSelectionCell(row, provider); appendProviderCell(row, provider); appendBadgeCell(row, provider.pair, "text-bg-primary"); appendBadgeCell(row, semanticsLabels[provider.semantics] ?? semanticsLabels.unknown, "text-bg-light", "d-none d-xl-table-cell"); appendBadgeCell(row, authLabels[provider.authMode] ?? authLabels.unknown, "text-bg-light", "d-none d-xl-table-cell"); appendAvailabilityCell(row, provider); appendPriceCell(row, provider); appendObservationTimeCell(row, provider); appendRetryCell(row, provider); appendRefreshCell(row, provider); tableBody.append(row); } synchronizeBatchControls(); frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", { rowCount: rows.length, observedCount: rows.filter(provider => provider.refreshed).length, attentionCount: rows.filter(provider => provider.availability !== "ready").length, }); } function selectAllProviders(selected: boolean): void { selectedProviderIds.clear(); if (selected) { for (const provider of currentMarketPriceRows) { selectedProviderIds.add(provider.providerId); } } frontendDebug("main", "SOL Prices Desk select-all providers control changed", { selected, selectedCount: selectedProviderIds.size, }); renderMarketPriceRows(currentMarketPriceRows); } function clearProviderSelection(): void { const selectedCount = selectedProviderIds.size; frontendDebug("main", "SOL Prices Desk clear provider selection control clicked", { selectedCount }); selectedProviderIds.clear(); renderMarketPriceRows(currentMarketPriceRows); } function bindMarketPriceControls(): void { const selectedButton = document.querySelector("#refreshSelectedButton"); const allButton = document.querySelector("#refreshAllButton"); const clearButton = document.querySelector("#clearSelectionButton"); const selectAll = document.querySelector("#selectAllProviders"); selectedButton?.addEventListener("click", () => { void refreshSelectedMarketPrices(); }); allButton?.addEventListener("click", () => { void refreshAllMarketPrices(); }); clearButton?.addEventListener("click", () => { clearProviderSelection(); }); selectAll?.addEventListener("change", () => { selectAllProviders(selectAll.checked); }); synchronizeBatchControls(); frontendTrace("main", "SOL Prices Desk market-price controls installed"); } function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void { const values: Record = { runtimeCompositeProfile: status.activeCompositeProfile, runtimeOffchainProfile: status.activeOffchainProfile, runtimeVersion: status.applicationVersion, runtimeShellPhase: status.shellPhase, runtimeConfigDocuments: status.configDocumentCount.toString(), runtimeProviderCount: status.providerCount.toString(), runtimeReadyProviderCount: status.readyProviderCount.toString(), runtimeUnavailableProviderCount: status.unavailableProviderCount.toString(), runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback", runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non", }; for (const [elementId, value] of Object.entries(values)) { const element = document.querySelector(`#${elementId}`); if (element) { element.textContent = value; } } const diagnostic = document.querySelector("#runtimeDiagnostic"); if (diagnostic) { diagnostic.hidden = status.startupDiagnostic === null; diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}:${status.startupDiagnostic.code} — ${status.startupDiagnostic.message}`; } frontendTrace("main", "SOL Prices Desk market-price runtime status rendered", { fallbackLoggingActive: status.fallbackLoggingActive, configDocumentCount: status.configDocumentCount, providerCount: status.providerCount, readyProviderCount: status.readyProviderCount, unavailableProviderCount: status.unavailableProviderCount, }); } async function loadMarketPrices(): Promise { frontendTrace("main", "SOL Prices Desk provider registry load started"); const rows = await invokeKsp("main", "list_market_prices"); currentMarketPriceRows = rows; renderMarketPriceRows(rows); frontendTrace("main", "SOL Prices Desk provider registry load completed", { rowCount: rows.length }); } async function loadRuntimeStatus(): Promise { const status = await invokeKsp("main", "get_runtime_status"); renderRuntimeStatus(status); } async function initializeMain(): Promise { const windowLabel = getCurrentWindow().label; frontendInfo("main", "SOL Prices Desk main frontend loaded", { windowLabel }); bindNavigation(); bindMarketPriceControls(); activateView("prices", "startup"); try { await loadRuntimeStatus(); } catch { const diagnostic = document.querySelector("#runtimeDiagnostic"); if (diagnostic) { diagnostic.hidden = false; diagnostic.textContent = "Le statut runtime initial n'a pas pu être chargé."; } frontendWarn("main", "SOL Prices Desk startup runtime status load failed"); } try { await loadMarketPrices(); } catch { const loading = document.querySelector("#pricesLoading"); if (loading) { loading.hidden = true; } showPricesDiagnostic("L'inventaire provider-neutral n'a pas pu être chargé."); frontendWarn("main", "SOL Prices Desk provider registry load failed"); } } document.addEventListener("DOMContentLoaded", () => { void initializeMain(); });