1031 lines
53 KiB
TypeScript
1031 lines
53 KiB
TypeScript
// 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<T> {
|
|
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<ViewId, string> = {
|
|
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 `<span class="app-copyable-long-text"><span class="app-copyable-long-text__value font-monospace" title="${escapedValue}">${escapedVisible}</span><button class="btn btn-sm btn-outline-secondary app-copyable-long-text__copy" type="button" data-copy-long-text="${escapedValue}" data-copy-field="${escapedFieldId}" title="Copier la valeur complète" aria-label="Copier la valeur complète"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
|
|
}
|
|
|
|
function setCopyableLongText(elementId: string, value: string, fieldId: string): void {
|
|
const element = document.querySelector<HTMLElement>(`#${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 = '<i class="fa-regular fa-copy" aria-hidden="true"></i>';
|
|
wrapper.append(text, button);
|
|
element.append(wrapper);
|
|
}
|
|
|
|
async function writeClipboardText(value: string): Promise<boolean> {
|
|
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<void> {
|
|
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<HTMLElement>("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<HTMLElement>(`#${elementId}`);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
}
|
|
|
|
function setVisible(elementId: string, visible: boolean): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (element) {
|
|
element.hidden = !visible;
|
|
}
|
|
}
|
|
|
|
function renderDiagnostic(elementId: string, diagnostic: { domain: string; code: string; message: string } | null): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = diagnostic === null;
|
|
element.textContent = diagnostic ? `${diagnostic.domain}/${diagnostic.code}: ${diagnostic.message}` : "";
|
|
}
|
|
|
|
function activateView(viewId: ViewId): void {
|
|
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
|
panel.hidden = panel.dataset.viewPanel !== viewId;
|
|
});
|
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
|
button.classList.toggle("active", button.dataset.view === viewId);
|
|
});
|
|
const title = document.querySelector<HTMLElement>("#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 `<span class="badge text-bg-secondary">${escapeHtml(origin)}</span>`;
|
|
}
|
|
|
|
function renderOptionalBoolean(value: boolean | null): string {
|
|
if (value === null) {
|
|
return "—";
|
|
}
|
|
return value ? '<span class="badge text-bg-info">oui</span>' : '<span class="badge text-bg-secondary">non</span>';
|
|
}
|
|
|
|
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<HTMLInputElement>("#transactionSlotMin");
|
|
const slotMaxElement = document.querySelector<HTMLInputElement>("#transactionSlotMax");
|
|
const directionElement = document.querySelector<HTMLSelectElement>("#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<StoreTransactionRowDto> {
|
|
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
|
}
|
|
|
|
function setTransactionQueryError(message: string | null): void {
|
|
const element = document.querySelector<HTMLElement>("#transactionsQueryError");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = message === null;
|
|
element.textContent = message ?? "";
|
|
}
|
|
|
|
async function queryTransactions(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreTransactionRowDto>) => void): Promise<void> {
|
|
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<StoreTransactionQueryResponseDto>("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 `<span class="badge ${className}">${label}</span>`;
|
|
}
|
|
|
|
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 `<button class="btn btn-sm btn-outline-primary" type="button" data-transaction-detail="${escapeHtml(transaction.signature)}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
|
|
},
|
|
},
|
|
],
|
|
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<HTMLInputElement>("#accountPubkey");
|
|
const slotMinElement = document.querySelector<HTMLInputElement>("#accountSlotMin");
|
|
const slotMaxElement = document.querySelector<HTMLInputElement>("#accountSlotMax");
|
|
const directionElement = document.querySelector<HTMLSelectElement>("#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<StoreAccountRowDto> {
|
|
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
|
}
|
|
|
|
function setAccountQueryError(message: string | null): void {
|
|
const element = document.querySelector<HTMLElement>("#accountsQueryError");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = message === null;
|
|
element.textContent = message ?? "";
|
|
}
|
|
|
|
async function queryAccounts(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreAccountRowDto>) => void): Promise<void> {
|
|
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<StoreAccountQueryResponseDto>("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) ? '<span class="badge text-bg-info">oui</span>' : '<span class="badge text-bg-secondary">non</span>') },
|
|
{ className: "font-monospace", data: "rentEpochDecimal" },
|
|
{ className: "font-monospace", data: "dataLengthDecimal" },
|
|
{
|
|
data: null,
|
|
defaultContent: "",
|
|
render: (_data, _type, row) => {
|
|
const account = row as StoreAccountRowDto;
|
|
return `<button class="btn btn-sm btn-outline-primary" type="button" data-account-detail data-account-pubkey="${escapeHtml(account.pubkey)}" data-account-slot="${escapeHtml(account.slotDecimal)}" data-account-state-hash="${escapeHtml(account.stateHash)}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
|
|
},
|
|
},
|
|
],
|
|
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<StoreTransactionObservationRowDto> {
|
|
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
|
}
|
|
|
|
function emptyAccountObservationPage(draw: number): DataTablesPageResponse<StoreAccountObservationRowDto> {
|
|
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
|
}
|
|
|
|
function setTransactionObservationError(message: string | null): void {
|
|
const element = document.querySelector<HTMLElement>("#transactionObservationsError");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = message === null;
|
|
element.textContent = message ?? "";
|
|
}
|
|
|
|
function setAccountObservationError(message: string | null): void {
|
|
const element = document.querySelector<HTMLElement>("#accountObservationsError");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = message === null;
|
|
element.textContent = message ?? "";
|
|
}
|
|
|
|
async function queryTransactionObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreTransactionObservationRowDto>) => void): Promise<void> {
|
|
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<StoreTransactionObservationQueryResponseDto>("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<StoreAccountObservationRowDto>) => void): Promise<void> {
|
|
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<StoreAccountObservationQueryResponseDto>("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<void> {
|
|
frontendDebug("main", "Store Desk Store runtime refresh started");
|
|
try {
|
|
const status = await invokeKsp<StoreRuntimeStatusDto>("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<void> {
|
|
frontendDebug("main", "Store Desk diagnostics refresh started");
|
|
try {
|
|
const status = await invokeKsp<ShellStatusDto>("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<HTMLButtonElement>("#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<HTMLButtonElement>("#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<void> {
|
|
const modalElement = document.querySelector<HTMLElement>("#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<StoreAccountDetailDto>("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<HTMLButtonElement>("#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<HTMLButtonElement>("#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<void> {
|
|
const modalElement = document.querySelector<HTMLElement>("#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<StoreTransactionDetailDto>("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<HTMLButtonElement>("[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<HTMLButtonElement>("#refreshOverview");
|
|
refreshOverview?.addEventListener("click", () => {
|
|
frontendDebug("main", "Store Desk Overview refresh button clicked");
|
|
void refreshStoreRuntime();
|
|
});
|
|
const refreshTransactions = document.querySelector<HTMLButtonElement>("#refreshTransactions");
|
|
refreshTransactions?.addEventListener("click", () => {
|
|
frontendDebug("main", "Store Desk RAW Transactions refresh button clicked");
|
|
redrawTransactions(false, "refresh");
|
|
});
|
|
const refreshAccounts = document.querySelector<HTMLButtonElement>("#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<HTMLElement>(`#${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<HTMLElement>(`#${elementId}`);
|
|
control?.addEventListener("change", () => {
|
|
frontendDebug("main", "Store Desk account Store filter changed", { controlId: elementId });
|
|
redrawAccounts(true, "store-filter");
|
|
});
|
|
}
|
|
const refreshButton = document.querySelector<HTMLButtonElement>("#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<HTMLButtonElement>("[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<HTMLElement>("[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<HTMLElement>("[data-transaction-detail]");
|
|
if (detailButton) {
|
|
const signature = detailButton.dataset.transactionDetail;
|
|
if (signature) {
|
|
void openTransactionDetail(signature);
|
|
}
|
|
return;
|
|
}
|
|
const control = eventTarget.closest<HTMLElement>("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<HTMLElement>("#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<HTMLElement>("#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<void> {
|
|
frontendInfo("main", "Store Desk main frontend loaded");
|
|
initializeTransactionTable();
|
|
initializeAccountTable();
|
|
initializeTransactionObservationTable();
|
|
initializeAccountObservationTable();
|
|
installInteractions();
|
|
activateView("overview");
|
|
await refreshDiagnostics();
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
void initializeMain();
|
|
});
|