// file: crates/ksp-app-store-desk/frontend/ts/main.ts // version: 8 import DataTable from "datatables.net-bs5"; import "datatables.net-bs5/css/dataTables.bootstrap5.css"; import { Modal } from "bootstrap"; import ResizeObserver from "resize-observer-polyfill"; import "simplebar"; import type { StoreAccountDetailDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountDetailDto.ts"; import type { StoreAccountDetailRequestDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountDetailRequestDto.ts"; import type { StoreAccountQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountQueryRequestDto.ts"; import type { StoreAccountQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountQueryResponseDto.ts"; import type { StoreAccountRowDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountRowDto.ts"; import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts"; import type { StoreRuntimeStatusDto } from "./bindings/ksp_app_store_desk/dto_common/StoreRuntimeStatusDto.ts"; import type { StoreAccountObservationQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryRequestDto.ts"; import type { StoreAccountObservationQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryResponseDto.ts"; import type { StoreAccountObservationRowDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationRowDto.ts"; import type { StoreTransactionObservationQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryRequestDto.ts"; import type { StoreTransactionObservationQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryResponseDto.ts"; import type { StoreTransactionObservationRowDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationRowDto.ts"; import type { StoreTransactionDetailDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts"; import type { StoreTransactionDetailRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailRequestDto.ts"; import type { StoreTransactionQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryRequestDto.ts"; import type { StoreTransactionQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryResponseDto.ts"; import type { StoreTransactionRowDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionRowDto.ts"; import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log"; import { invokeKsp } from "./invoke"; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; installFrontendConsoleBridge("main"); type ViewId = "overview" | "transactions" | "accounts" | "diagnostics"; interface DataTablesPageRequest { draw: number; length: number; start: number; } interface DataTablesPageResponse { data: T[]; draw: number; recordsFiltered: number; recordsTotal: number; } interface StoreDataTable { columns: { adjust(): void }; draw(resetPaging?: boolean): void; on(event: string, callback: (...args: unknown[]) => void): StoreDataTable; } const viewTitles: Record = { overview: "Overview", transactions: "RAW Transactions", accounts: "RAW Accounts", diagnostics: "Diagnostics", }; let transactionTable: StoreDataTable | null = null; let accountTable: StoreDataTable | null = null; let transactionObservationTable: StoreDataTable | null = null; let accountObservationTable: StoreDataTable | null = null; let currentTransactionObservationSignature: string | null = null; let currentAccountObservationIdentity: { pubkey: string; slot: string; stateHash: string } | null = null; const COPY_FEEDBACK_MILLISECONDS = 1200; const LONG_TEXT_HEAD_CHARACTERS = 12; const LONG_TEXT_TAIL_CHARACTERS = 8; function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll('"', """) .replaceAll("'", "'") .replaceAll("<", "<") .replaceAll(">", ">"); } function truncateLongText(value: string): string { const threshold = LONG_TEXT_HEAD_CHARACTERS + LONG_TEXT_TAIL_CHARACTERS + 1; if (value.length <= threshold) { return value; } return `${value.slice(0, LONG_TEXT_HEAD_CHARACTERS)}…${value.slice(-LONG_TEXT_TAIL_CHARACTERS)}`; } function renderCopyableLongText(value: string, fieldId: string): string { const escapedValue = escapeHtml(value); const escapedVisible = escapeHtml(truncateLongText(value)); const escapedFieldId = escapeHtml(fieldId); return `${escapedVisible}`; } function setCopyableLongText(elementId: string, value: string, fieldId: string): void { const element = document.querySelector(`#${elementId}`); if (!element) { return; } element.replaceChildren(); const wrapper = document.createElement("span"); wrapper.className = "app-copyable-long-text"; const text = document.createElement("span"); text.className = "app-copyable-long-text__value font-monospace"; text.textContent = truncateLongText(value); text.title = value; const button = document.createElement("button"); button.className = "btn btn-sm btn-outline-secondary app-copyable-long-text__copy"; button.type = "button"; button.dataset.copyLongText = value; button.dataset.copyField = fieldId; button.title = "Copier la valeur complète"; button.setAttribute("aria-label", "Copier la valeur complète"); button.innerHTML = ''; wrapper.append(text, button); element.append(wrapper); } async function writeClipboardText(value: string): Promise { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { try { await navigator.clipboard.writeText(value); return true; } catch { // Fall back to the document copy command for WebViews without Clipboard API permission. } } const textarea = document.createElement("textarea"); textarea.value = value; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.left = "-10000px"; textarea.style.top = "-10000px"; document.body.append(textarea); textarea.select(); const copied = document.execCommand("copy"); textarea.remove(); return copied; } async function copyLongText(button: HTMLButtonElement, value: string, fieldId: string): Promise { frontendDebug("main", "Store Desk long text copy requested", { fieldId }); const copied = await writeClipboardText(value); if (!copied) { frontendError("main", "Store Desk long text copy failed", { fieldId }); return; } const icon = button.querySelector("i"); const previousClassName = icon?.className ?? ""; const previousTitle = button.title; if (icon) { icon.className = "fa-solid fa-check"; } button.title = "Copié"; frontendDebug("main", "Store Desk long text copy completed", { fieldId }); window.setTimeout(() => { if (icon) { icon.className = previousClassName; } button.title = previousTitle; }, COPY_FEEDBACK_MILLISECONDS); } function isViewId(value: string | undefined): value is ViewId { return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics"; } function setText(elementId: string, value: string): void { const element = document.querySelector(`#${elementId}`); if (element) { element.textContent = value; } } function setVisible(elementId: string, visible: boolean): void { const element = document.querySelector(`#${elementId}`); if (element) { element.hidden = !visible; } } function renderDiagnostic(elementId: string, diagnostic: { domain: string; code: string; message: string } | null): void { const element = document.querySelector(`#${elementId}`); if (!element) { return; } element.hidden = diagnostic === null; element.textContent = diagnostic ? `${diagnostic.domain}/${diagnostic.code}: ${diagnostic.message}` : ""; } function activateView(viewId: ViewId): 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 title = document.querySelector("#headerViewTitle"); if (title) { title.textContent = viewTitles[viewId]; } document.title = `Store Desk — ${viewTitles[viewId]}`; if (viewId === "transactions") { transactionTable?.columns.adjust(); } if (viewId === "accounts") { accountTable?.columns.adjust(); } frontendDebug("main", "Store Desk view activated", { viewId }); } function decimalCountToSafeNumber(value: string): number | null { if (!/^\d+$/.test(value)) { return null; } const exact = BigInt(value); if (exact > BigInt(Number.MAX_SAFE_INTEGER)) { return null; } return Number(exact); } function optionalLongText(value: string | null, fieldId: string): string { return value === null ? "—" : renderCopyableLongText(value, fieldId); } function observationPageRequest(page: DataTablesPageRequest): { draw: number; limit: number; offset: string } | null { if (!Number.isSafeInteger(page.draw) || page.draw < 0 || !Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) { return null; } return { draw: page.draw, limit: page.length, offset: page.start.toString() }; } function renderObservationCode(value: string | null, fieldId: string): string { return optionalLongText(value, fieldId); } function renderOrigin(origin: string): string { return `${escapeHtml(origin)}`; } function renderOptionalBoolean(value: boolean | null): string { if (value === null) { return "—"; } return value ? 'oui' : 'non'; } function currentTransactionQuery(page: DataTablesPageRequest): StoreTransactionQueryRequestDto | null { if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) { return null; } const slotMinElement = document.querySelector("#transactionSlotMin"); const slotMaxElement = document.querySelector("#transactionSlotMax"); const directionElement = document.querySelector("#transactionDirection"); const slotMin = slotMinElement?.value.trim() ?? ""; const slotMax = slotMaxElement?.value.trim() ?? ""; return { direction: directionElement?.value ?? "descending", limit: page.length, offset: page.start.toString(), slotMax: slotMax === "" ? null : slotMax, slotMin: slotMin === "" ? null : slotMin, }; } function emptyTransactionPage(draw: number): DataTablesPageResponse { return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 }; } function setTransactionQueryError(message: string | null): void { const element = document.querySelector("#transactionsQueryError"); if (!element) { return; } element.hidden = message === null; element.textContent = message ?? ""; } async function queryTransactions(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse) => void): Promise { const draw = Number.isSafeInteger(page.draw) && page.draw >= 0 ? page.draw : 0; const request = currentTransactionQuery(page); if (!request) { setTransactionQueryError("La pagination demandée n'est pas admise par Store Desk."); callback(emptyTransactionPage(draw)); frontendError("main", "Store Desk transaction DataTables query rejected before IPC"); return; } setTransactionQueryError(null); frontendDebug("main", "Store Desk transaction DataTables server-side query started", { pageLength: request.limit, slotFilterActive: request.slotMin !== null || request.slotMax !== null, }); try { const response = await invokeKsp("main", "store_query_transactions", { request }); const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal); const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal); if (recordsTotal === null || recordsFiltered === null) { setTransactionQueryError("Le volume Store dépasse le domaine entier sûr de l'interface."); callback(emptyTransactionPage(draw)); frontendError("main", "Store Desk transaction DataTables exact count exceeds frontend safe integer domain"); return; } callback({ data: response.rows, draw, recordsFiltered, recordsTotal }); frontendDebug("main", "Store Desk transaction DataTables server-side query completed", { rowCount: response.rows.length }); } catch { setTransactionQueryError("La requête RAW Transactions a échoué."); callback(emptyTransactionPage(draw)); frontendError("main", "Store Desk transaction DataTables server-side query failed"); } } function retentionBadge(retentionState: string): string { const label = retentionState === "full" ? "Full" : retentionState === "archived" ? "Archived" : retentionState === "purged" ? "Purged" : retentionState; const className = retentionState === "full" ? "text-bg-success" : retentionState === "archived" ? "text-bg-info" : retentionState === "purged" ? "text-bg-secondary" : "text-bg-warning"; return `${label}`; } function initializeTransactionTable(): void { transactionTable = new DataTable("#rawTransactionsTable", { ajax: (data, callback) => { const request = data as unknown as DataTablesPageRequest; void queryTransactions(request, response => callback(response)); }, autoWidth: false, columns: [ { data: "signature", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-signature") : String(data)) }, { className: "font-monospace", data: "slotDecimal" }, { data: "blockTimeUnixMillisDecimal", defaultContent: "—" }, { data: "formatId" }, { data: "formatVersion" }, { data: "payloadSizeDecimal", defaultContent: "—" }, { data: "contentHash", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-content-hash") : String(data)) }, { data: "retentionState", render: data => retentionBadge(String(data)) }, { data: null, defaultContent: "", render: (_data, _type, row) => { const transaction = row as StoreTransactionRowDto; return ``; }, }, ], info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true, language: { emptyTable: "Aucune transaction RAW ne correspond à la requête Store.", info: "_START_ à _END_ sur _TOTAL_ transaction(s)", infoEmpty: "0 transaction", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucune transaction RAW correspondante.", }, }) as unknown as StoreDataTable; transactionTable.on("page", () => { frontendDebug("main", "Store Desk transaction DataTables page changed"); }); transactionTable.on("length", () => { frontendDebug("main", "Store Desk transaction DataTables page length changed"); }); transactionTable.on("draw", () => { frontendTrace("main", "Store Desk transaction DataTables redraw completed"); }); frontendTrace("main", "Store Desk transaction DataTable initialized", { pagingOwner: "datatables", serverSide: true }); } function currentAccountQuery(page: DataTablesPageRequest): StoreAccountQueryRequestDto | null { if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) { return null; } const pubkeyElement = document.querySelector("#accountPubkey"); const slotMinElement = document.querySelector("#accountSlotMin"); const slotMaxElement = document.querySelector("#accountSlotMax"); const directionElement = document.querySelector("#accountDirection"); const pubkey = pubkeyElement?.value.trim() ?? ""; const slotMin = slotMinElement?.value.trim() ?? ""; const slotMax = slotMaxElement?.value.trim() ?? ""; return { direction: directionElement?.value ?? "descending", limit: page.length, offset: page.start.toString(), pubkey: pubkey === "" ? null : pubkey, slotMax: slotMax === "" ? null : slotMax, slotMin: slotMin === "" ? null : slotMin, }; } function emptyAccountPage(draw: number): DataTablesPageResponse { return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 }; } function setAccountQueryError(message: string | null): void { const element = document.querySelector("#accountsQueryError"); if (!element) { return; } element.hidden = message === null; element.textContent = message ?? ""; } async function queryAccounts(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse) => void): Promise { const draw = Number.isSafeInteger(page.draw) && page.draw >= 0 ? page.draw : 0; const request = currentAccountQuery(page); if (!request) { setAccountQueryError("La pagination demandée n'est pas admise par Store Desk."); callback(emptyAccountPage(draw)); frontendError("main", "Store Desk account DataTables query rejected before IPC"); return; } setAccountQueryError(null); frontendDebug("main", "Store Desk account DataTables server-side query started", { pageLength: request.limit, pubkeyFilterActive: request.pubkey !== null, slotFilterActive: request.slotMin !== null || request.slotMax !== null, }); try { const response = await invokeKsp("main", "store_query_accounts", { request }); const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal); const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal); if (recordsTotal === null || recordsFiltered === null) { setAccountQueryError("Le volume Store dépasse le domaine entier sûr de l'interface."); callback(emptyAccountPage(draw)); frontendError("main", "Store Desk account DataTables exact count exceeds frontend safe integer domain"); return; } callback({ data: response.rows, draw, recordsFiltered, recordsTotal }); frontendDebug("main", "Store Desk account DataTables server-side query completed", { rowCount: response.rows.length }); } catch { setAccountQueryError("La requête RAW Accounts a échoué."); callback(emptyAccountPage(draw)); frontendError("main", "Store Desk account DataTables server-side query failed"); } } function initializeAccountTable(): void { accountTable = new DataTable("#rawAccountsTable", { ajax: (data, callback) => { const request = data as unknown as DataTablesPageRequest; void queryAccounts(request, response => callback(response)); }, autoWidth: false, columns: [ { data: "pubkey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-pubkey") : String(data)) }, { className: "font-monospace", data: "slotDecimal" }, { data: "stateHash", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-state-hash") : String(data)) }, { data: "owner", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-owner") : String(data)) }, { className: "font-monospace", data: "lamportsDecimal" }, { data: "executable", render: data => (Boolean(data) ? 'oui' : 'non') }, { className: "font-monospace", data: "rentEpochDecimal" }, { className: "font-monospace", data: "dataLengthDecimal" }, { data: null, defaultContent: "", render: (_data, _type, row) => { const account = row as StoreAccountRowDto; return ``; }, }, ], info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true, language: { emptyTable: "Aucun état RAW Account ne correspond à la requête Store.", info: "_START_ à _END_ sur _TOTAL_ état(s) Account", infoEmpty: "0 état Account", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucun état RAW Account correspondant.", }, }) as unknown as StoreDataTable; accountTable.on("page", () => { frontendDebug("main", "Store Desk account DataTables page changed"); }); accountTable.on("length", () => { frontendDebug("main", "Store Desk account DataTables page length changed"); }); accountTable.on("draw", () => { frontendTrace("main", "Store Desk account DataTables redraw completed"); }); frontendTrace("main", "Store Desk account DataTable initialized", { pagingOwner: "datatables", serverSide: true }); } function emptyTransactionObservationPage(draw: number): DataTablesPageResponse { return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 }; } function emptyAccountObservationPage(draw: number): DataTablesPageResponse { return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 }; } function setTransactionObservationError(message: string | null): void { const element = document.querySelector("#transactionObservationsError"); if (!element) { return; } element.hidden = message === null; element.textContent = message ?? ""; } function setAccountObservationError(message: string | null): void { const element = document.querySelector("#accountObservationsError"); if (!element) { return; } element.hidden = message === null; element.textContent = message ?? ""; } async function queryTransactionObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse) => void): Promise { const pagination = observationPageRequest(page); const signature = currentTransactionObservationSignature; if (signature === null) { callback(emptyTransactionObservationPage(pagination?.draw ?? 0)); return; } if (pagination === null) { setTransactionObservationError("La pagination des observations n'est pas admise par Store Desk."); callback(emptyTransactionObservationPage(0)); frontendError("main", "Store Desk transaction observation DataTables query rejected before IPC"); return; } const request: StoreTransactionObservationQueryRequestDto = { limit: pagination.limit, offset: pagination.offset, signature }; setTransactionObservationError(null); frontendDebug("main", "Store Desk transaction observation DataTables server-side query started", { pageLength: request.limit }); try { const response = await invokeKsp("main", "store_query_transaction_observations", { request }); if (currentTransactionObservationSignature !== signature) { callback(emptyTransactionObservationPage(pagination.draw)); frontendTrace("main", "Store Desk stale transaction observation response ignored"); return; } const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal); const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal); if (recordsTotal === null || recordsFiltered === null) { setTransactionObservationError("Le volume d'observations dépasse le domaine entier sûr de l'interface."); callback(emptyTransactionObservationPage(pagination.draw)); frontendError("main", "Store Desk transaction observation exact count exceeds frontend safe integer domain"); return; } callback({ data: response.rows, draw: pagination.draw, recordsFiltered, recordsTotal }); frontendDebug("main", "Store Desk transaction observation DataTables server-side query completed", { rowCount: response.rows.length }); } catch { setTransactionObservationError("La requête d'observations Transaction a échoué."); callback(emptyTransactionObservationPage(pagination.draw)); frontendError("main", "Store Desk transaction observation DataTables server-side query failed"); } } async function queryAccountObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse) => void): Promise { const pagination = observationPageRequest(page); const identity = currentAccountObservationIdentity; if (identity === null) { callback(emptyAccountObservationPage(pagination?.draw ?? 0)); return; } if (pagination === null) { setAccountObservationError("La pagination des observations n'est pas admise par Store Desk."); callback(emptyAccountObservationPage(0)); frontendError("main", "Store Desk account observation DataTables query rejected before IPC"); return; } const request: StoreAccountObservationQueryRequestDto = { limit: pagination.limit, offset: pagination.offset, pubkey: identity.pubkey, slot: identity.slot, stateHash: identity.stateHash, }; setAccountObservationError(null); frontendDebug("main", "Store Desk account observation DataTables server-side query started", { pageLength: request.limit }); try { const response = await invokeKsp("main", "store_query_account_observations", { request }); if (currentAccountObservationIdentity !== identity) { callback(emptyAccountObservationPage(pagination.draw)); frontendTrace("main", "Store Desk stale account observation response ignored"); return; } const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal); const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal); if (recordsTotal === null || recordsFiltered === null) { setAccountObservationError("Le volume d'observations dépasse le domaine entier sûr de l'interface."); callback(emptyAccountObservationPage(pagination.draw)); frontendError("main", "Store Desk account observation exact count exceeds frontend safe integer domain"); return; } callback({ data: response.rows, draw: pagination.draw, recordsFiltered, recordsTotal }); frontendDebug("main", "Store Desk account observation DataTables server-side query completed", { rowCount: response.rows.length }); } catch { setAccountObservationError("La requête d'observations Account a échoué."); callback(emptyAccountObservationPage(pagination.draw)); frontendError("main", "Store Desk account observation DataTables server-side query failed"); } } function provenanceColumns(prefix: string) { return [ { className: "font-monospace", data: "provenance.receivedAtUnixMillisDecimal" }, { className: "font-monospace", data: "provenance.observedAtUnixMillisDecimal", defaultContent: "—" }, { data: "provenance.provider", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-provider`) : String(data)) }, { data: "provenance.protocol", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-protocol`) : String(data)) }, { data: "provenance.acquisitionMethod", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-method`) : String(data)) }, { data: "provenance.origin", render: (data: unknown, type: string) => (type === "display" ? renderOrigin(String(data)) : String(data)) }, { data: "provenance.endpointId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-endpoint`) : data) }, { data: "provenance.commitment", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-commitment`) : data) }, { data: "provenance.captureSessionId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-session`) : data) }, { data: "provenance.filterId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-filter`) : data) }, { data: "provenance.sourcePayloadHash", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-source-hash`) : data) }, { className: "font-monospace", data: "provenance.sourcePayloadSizeDecimal", defaultContent: "—" }, ]; } function initializeTransactionObservationTable(): void { transactionObservationTable = new DataTable("#transactionObservationsTable", { ajax: (data, callback) => { void queryTransactionObservations(data as unknown as DataTablesPageRequest, response => callback(response)); }, autoWidth: false, columns: [ ...provenanceColumns("transaction-observation"), { data: "observationKey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-observation-key") : String(data)) }, ], info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true, language: { emptyTable: "Aucune observation pour cette transaction.", info: "_START_ à _END_ sur _TOTAL_ observation(s)", infoEmpty: "0 observation", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucune observation correspondante." }, }) as unknown as StoreDataTable; transactionObservationTable.on("page", () => frontendDebug("main", "Store Desk transaction observation DataTables page changed")); transactionObservationTable.on("length", () => frontendDebug("main", "Store Desk transaction observation DataTables page length changed")); transactionObservationTable.on("draw", () => frontendTrace("main", "Store Desk transaction observation DataTables redraw completed")); frontendTrace("main", "Store Desk transaction observation DataTable initialized", { pagingOwner: "datatables", serverSide: true }); } function initializeAccountObservationTable(): void { accountObservationTable = new DataTable("#accountObservationsTable", { ajax: (data, callback) => { void queryAccountObservations(data as unknown as DataTablesPageRequest, response => callback(response)); }, autoWidth: false, columns: [ ...provenanceColumns("account-observation"), { data: "observationKey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-observation-key") : String(data)) }, { data: "isStartup", defaultContent: null, render: (data, type) => (type === "display" ? renderOptionalBoolean(data === null ? null : Boolean(data)) : data) }, { className: "font-monospace", data: "writeVersionDecimal", defaultContent: "—" }, { data: "transactionSignature", defaultContent: null, render: (data, type) => (type === "display" ? optionalLongText(data === null ? null : String(data), "account-observation-transaction-signature") : data) }, ], info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true, language: { emptyTable: "Aucune observation pour cet état Account.", info: "_START_ à _END_ sur _TOTAL_ observation(s)", infoEmpty: "0 observation", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucune observation correspondante." }, }) as unknown as StoreDataTable; accountObservationTable.on("page", () => frontendDebug("main", "Store Desk account observation DataTables page changed")); accountObservationTable.on("length", () => frontendDebug("main", "Store Desk account observation DataTables page length changed")); accountObservationTable.on("draw", () => frontendTrace("main", "Store Desk account observation DataTables redraw completed")); frontendTrace("main", "Store Desk account observation DataTable initialized", { pagingOwner: "datatables", serverSide: true }); } function renderShellStatus(status: ShellStatusDto): void { setText("appVersionBadge", status.applicationVersion); setText("runtimeVersion", status.applicationVersion); setText("runtimeShellPhase", status.shellPhase); setText("runtimeConfigDocuments", status.configDocumentCount.toString()); setText("runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback"); setText("runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non"); renderDiagnostic("runtimeDiagnostic", status.startupDiagnostic); frontendTrace("main", "Store Desk shell status rendered", { fallbackLoggingActive: status.fallbackLoggingActive, shellPhase: status.shellPhase, }); } function renderStoreRuntimeStatus(status: StoreRuntimeStatusDto): void { const target = status.backendKind && status.network ? `${status.backendKind} / ${status.network}` : status.network ?? status.backendKind ?? "indisponible"; const migration = status.migrationVersionDecimal === null ? `inconnue · ${status.pendingMigrationCount} pending` : `v${status.migrationVersionDecimal} · ${status.pendingMigrationCount} pending`; const pool = `${status.poolAvailable}/${status.poolSize}/${status.poolCapacity} · wait ${status.poolWaiting}`; const profile = status.profileId ?? "indisponible"; setText("overviewStoreProfile", profile); setText("overviewStoreTarget", target); setText("overviewStoreHealth", status.healthState); setText("overviewStorePool", pool); setText("overviewStoreMigration", migration); renderDiagnostic("overviewStoreDiagnostic", status.diagnostic); setText("runtimeStoreProfile", profile); setText("runtimeStoreTarget", target); setText("runtimeStoreHealth", status.healthState); setText("runtimeStoreMigration", migration); setText("runtimeStorePool", pool); renderDiagnostic("runtimeStoreDiagnostic", status.diagnostic); frontendTrace("main", "Store Desk Store runtime status rendered", { healthState: status.healthState, pendingMigrationCount: status.pendingMigrationCount, poolCapacity: status.poolCapacity, poolSize: status.poolSize, storeOpen: status.storeOpen, }); } async function refreshStoreRuntime(): Promise { frontendDebug("main", "Store Desk Store runtime refresh started"); try { const status = await invokeKsp("main", "store_runtime_status"); renderStoreRuntimeStatus(status); frontendDebug("main", "Store Desk Store runtime refresh completed", { healthState: status.healthState, storeOpen: status.storeOpen }); } catch { frontendError("main", "Store Desk Store runtime refresh failed"); } } async function refreshDiagnostics(): Promise { frontendDebug("main", "Store Desk diagnostics refresh started"); try { const status = await invokeKsp("main", "get_shell_status"); renderShellStatus(status); await refreshStoreRuntime(); frontendDebug("main", "Store Desk diagnostics refresh completed"); } catch { frontendError("main", "Store Desk diagnostics refresh failed"); } } function clearAccountDetail(): void { currentAccountObservationIdentity = null; setAccountObservationError(null); accountObservationTable?.draw(true); for (const elementId of [ "accountDetailPubkey", "accountDetailSlot", "accountDetailStateHash", "accountDetailOwner", "accountDetailLamports", "accountDetailExecutable", "accountDetailRentEpoch", "accountDetailDataLength", "accountDetailDataPreview", ]) { setText(elementId, "—"); } setVisible("accountDetailError", false); setVisible("accountDetailPreviewTruncated", false); const dataPreviewCopy = document.querySelector("#accountDetailDataPreviewCopy"); if (dataPreviewCopy) { delete dataPreviewCopy.dataset.copyLongText; delete dataPreviewCopy.dataset.copyField; dataPreviewCopy.hidden = true; } } function renderAccountDetail(detail: StoreAccountDetailDto): void { setCopyableLongText("accountDetailPubkey", detail.pubkey, "account-detail-pubkey"); setText("accountDetailSlot", detail.slotDecimal); setCopyableLongText("accountDetailStateHash", detail.stateHash, "account-detail-state-hash"); setCopyableLongText("accountDetailOwner", detail.owner, "account-detail-owner"); setText("accountDetailLamports", detail.lamportsDecimal); setText("accountDetailExecutable", detail.executable ? "oui" : "non"); setText("accountDetailRentEpoch", detail.rentEpochDecimal); setText("accountDetailDataLength", detail.dataLengthDecimal); const dataPreview = detail.dataPreviewHex; setText("accountDetailDataPreview", dataPreview === "" ? "Données Account vides." : dataPreview); const dataPreviewCopy = document.querySelector("#accountDetailDataPreviewCopy"); if (dataPreviewCopy && dataPreview !== "") { dataPreviewCopy.dataset.copyLongText = dataPreview; dataPreviewCopy.dataset.copyField = "account-data-preview"; dataPreviewCopy.hidden = false; } else if (dataPreviewCopy) { delete dataPreviewCopy.dataset.copyLongText; delete dataPreviewCopy.dataset.copyField; dataPreviewCopy.hidden = true; } setVisible("accountDetailPreviewTruncated", detail.dataPreviewTruncated); } async function openAccountDetail(pubkey: string, slot: string, stateHash: string): Promise { const modalElement = document.querySelector("#accountDetailModal"); if (!modalElement) { return; } clearAccountDetail(); setCopyableLongText("accountDetailPubkey", pubkey, "account-detail-pubkey"); setText("accountDetailSlot", slot); setCopyableLongText("accountDetailStateHash", stateHash, "account-detail-state-hash"); frontendDebug("main", "Store Desk account detail load requested"); const modal = Modal.getOrCreateInstance(modalElement); modal.show(); const request: StoreAccountDetailRequestDto = { pubkey, slot, stateHash }; try { const detail = await invokeKsp("main", "store_get_account_detail", { request }); renderAccountDetail(detail); currentAccountObservationIdentity = { pubkey, slot, stateHash }; accountObservationTable?.draw(true); frontendDebug("main", "Store Desk account detail opened", { dataPreviewTruncated: detail.dataPreviewTruncated }); } catch { setText("accountDetailError", "Le détail de cet état Account n'a pas pu être chargé."); setVisible("accountDetailError", true); frontendError("main", "Store Desk account detail load failed"); } } function redrawAccounts(resetPaging: boolean, reason: string): void { frontendDebug("main", "Store Desk account DataTables redraw requested", { reason }); accountTable?.draw(resetPaging); } function clearTransactionDetail(): void { currentTransactionObservationSignature = null; setTransactionObservationError(null); transactionObservationTable?.draw(true); for (const elementId of [ "transactionDetailSignature", "transactionDetailSlot", "transactionDetailBlockTime", "transactionDetailFormat", "transactionDetailContentHash", "transactionDetailRetention", "transactionDetailPayloadSize", "transactionDetailPayloadPreview", ]) { setText(elementId, "—"); } setVisible("transactionDetailError", false); setVisible("transactionDetailPreviewTruncated", false); const payloadPreviewCopy = document.querySelector("#transactionDetailPayloadPreviewCopy"); if (payloadPreviewCopy) { delete payloadPreviewCopy.dataset.copyLongText; delete payloadPreviewCopy.dataset.copyField; payloadPreviewCopy.hidden = true; } } function renderTransactionDetail(detail: StoreTransactionDetailDto): void { setCopyableLongText("transactionDetailSignature", detail.signature, "transaction-detail-signature"); setText("transactionDetailSlot", detail.slotDecimal); setText("transactionDetailBlockTime", detail.blockTimeUnixMillisDecimal ?? "—"); setText("transactionDetailFormat", `${detail.formatId} v${detail.formatVersion}`); setCopyableLongText("transactionDetailContentHash", detail.contentHash, "transaction-detail-content-hash"); setText("transactionDetailRetention", detail.retentionState); setText("transactionDetailPayloadSize", detail.payloadSizeDecimal ?? "—"); const payloadPreview = detail.payloadPreviewHex; setText("transactionDetailPayloadPreview", payloadPreview ?? "Payload non disponible dans cet état de rétention."); const payloadPreviewCopy = document.querySelector("#transactionDetailPayloadPreviewCopy"); if (payloadPreviewCopy && payloadPreview) { payloadPreviewCopy.dataset.copyLongText = payloadPreview; payloadPreviewCopy.dataset.copyField = "transaction-payload-preview"; payloadPreviewCopy.hidden = false; } else if (payloadPreviewCopy) { delete payloadPreviewCopy.dataset.copyLongText; delete payloadPreviewCopy.dataset.copyField; payloadPreviewCopy.hidden = true; } setVisible("transactionDetailPreviewTruncated", detail.payloadPreviewTruncated); } async function openTransactionDetail(signature: string): Promise { const modalElement = document.querySelector("#transactionDetailModal"); if (!modalElement) { return; } clearTransactionDetail(); setCopyableLongText("transactionDetailSignature", signature, "transaction-detail-signature"); frontendDebug("main", "Store Desk transaction detail load requested"); const modal = Modal.getOrCreateInstance(modalElement); modal.show(); const request: StoreTransactionDetailRequestDto = { signature }; try { const detail = await invokeKsp("main", "store_get_transaction_detail", { request }); renderTransactionDetail(detail); currentTransactionObservationSignature = signature; transactionObservationTable?.draw(true); frontendDebug("main", "Store Desk transaction detail opened", { payloadPreviewTruncated: detail.payloadPreviewTruncated, retentionState: detail.retentionState }); } catch { setText("transactionDetailError", "Le détail de cette transaction n'a pas pu être chargé."); setVisible("transactionDetailError", true); frontendError("main", "Store Desk transaction detail load failed"); } } function redrawTransactions(resetPaging: boolean, reason: string): void { frontendDebug("main", "Store Desk transaction DataTables redraw requested", { reason }); transactionTable?.draw(resetPaging); } function installInteractions(): void { document.querySelectorAll("[data-view]").forEach(button => { button.addEventListener("click", () => { const viewId = button.dataset.view; frontendDebug("main", "Store Desk navigation button clicked", { viewId: viewId ?? "missing" }); if (isViewId(viewId)) { activateView(viewId); } }); }); const refreshOverview = document.querySelector("#refreshOverview"); refreshOverview?.addEventListener("click", () => { frontendDebug("main", "Store Desk Overview refresh button clicked"); void refreshStoreRuntime(); }); const refreshTransactions = document.querySelector("#refreshTransactions"); refreshTransactions?.addEventListener("click", () => { frontendDebug("main", "Store Desk RAW Transactions refresh button clicked"); redrawTransactions(false, "refresh"); }); const refreshAccounts = document.querySelector("#refreshAccounts"); refreshAccounts?.addEventListener("click", () => { frontendDebug("main", "Store Desk RAW Accounts refresh button clicked"); redrawAccounts(false, "refresh"); }); for (const elementId of ["transactionSlotMin", "transactionSlotMax", "transactionDirection"]) { const control = document.querySelector(`#${elementId}`); control?.addEventListener("change", () => { frontendDebug("main", "Store Desk transaction Store filter changed", { controlId: elementId }); redrawTransactions(true, "store-filter"); }); } for (const elementId of ["accountPubkey", "accountSlotMin", "accountSlotMax", "accountDirection"]) { const control = document.querySelector(`#${elementId}`); control?.addEventListener("change", () => { frontendDebug("main", "Store Desk account Store filter changed", { controlId: elementId }); redrawAccounts(true, "store-filter"); }); } const refreshButton = document.querySelector("#refreshDiagnostics"); refreshButton?.addEventListener("click", () => { frontendDebug("main", "Store Desk diagnostics refresh button clicked"); void refreshDiagnostics(); }); document.addEventListener("click", event => { const eventTarget = event.target; if (!(eventTarget instanceof Element)) { return; } const copyButton = eventTarget.closest("[data-copy-long-text]"); if (copyButton) { const value = copyButton.dataset.copyLongText; const fieldId = copyButton.dataset.copyField ?? "long-text"; if (value) { void copyLongText(copyButton, value, fieldId); } return; } const accountDetailButton = eventTarget.closest("[data-account-detail]"); if (accountDetailButton) { const pubkey = accountDetailButton.dataset.accountPubkey; const slot = accountDetailButton.dataset.accountSlot; const stateHash = accountDetailButton.dataset.accountStateHash; if (pubkey && slot && stateHash) { void openAccountDetail(pubkey, slot, stateHash); } return; } const detailButton = eventTarget.closest("[data-transaction-detail]"); if (detailButton) { const signature = detailButton.dataset.transactionDetail; if (signature) { void openTransactionDetail(signature); } return; } const control = eventTarget.closest("button, [role='tab'], [data-bs-toggle='tab']"); if (!control) { return; } if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions" || control.id === "refreshAccounts")) { return; } const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous"; if (control.matches("[role='tab'], [data-bs-toggle='tab']")) { frontendDebug("main", "Store Desk tab control clicked", { controlId }); return; } frontendDebug("main", "Store Desk generic button clicked", { buttonId: controlId }); }); document.addEventListener("shown.bs.tab", event => { const target = event.target; const tabId = target instanceof HTMLElement ? target.id || target.getAttribute("data-bs-target") || target.textContent?.trim() || "anonymous" : "unknown"; frontendDebug("main", "Store Desk tab activated", { tabId }); }); document.addEventListener("change", event => { const target = event.target; if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement)) { return; } frontendDebug("main", "Store Desk interactive control changed", { controlId: target.id || target.getAttribute("name") || "anonymous", controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text", }); }); const accountDetailModal = document.querySelector("#accountDetailModal"); accountDetailModal?.addEventListener("shown.bs.modal", () => { accountObservationTable?.columns.adjust(); }); accountDetailModal?.addEventListener("hidden.bs.modal", () => { clearAccountDetail(); frontendDebug("main", "Store Desk account detail closed"); }); const detailModal = document.querySelector("#transactionDetailModal"); detailModal?.addEventListener("shown.bs.modal", () => { transactionObservationTable?.columns.adjust(); }); detailModal?.addEventListener("hidden.bs.modal", () => { clearTransactionDetail(); frontendDebug("main", "Store Desk transaction detail closed"); }); frontendTrace("main", "Store Desk frontend interactions installed", { buttons: true, controls: true, datatables: true, tabs: true }); } async function initializeMain(): Promise { frontendInfo("main", "Store Desk main frontend loaded"); initializeTransactionTable(); initializeAccountTable(); initializeTransactionObservationTable(); initializeAccountObservationTable(); installInteractions(); activateView("overview"); await refreshDiagnostics(); } document.addEventListener("DOMContentLoaded", () => { void initializeMain(); });