v0.3.8-pre.008

This commit is contained in:
2026-09-03 19:53:25 +02:00
parent 10515d1562
commit 489d686023
16 changed files with 1050 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
<!-- version: 4 -->
<!-- version: 5 -->
<!DOCTYPE html>
<html lang="fr">
@@ -127,14 +127,57 @@
</section>
<section data-view-panel="accounts" hidden>
<div class="mb-4">
<h1 class="h3 mb-1">RAW Accounts</h1>
<p class="text-body-secondary mb-0">Structure DataTables conservée hors connexion métier jusqu'à pre.008. Aucun byte Account ne traverse IPC dans cette tranche.</p>
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
<div>
<h1 class="h3 mb-1">RAW Accounts</h1>
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
</div>
<button id="refreshAccounts" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<label class="form-label" for="accountPubkey">Pubkey exacte</label>
<input id="accountPubkey" class="form-control font-monospace" type="text" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-2">
<label class="form-label" for="accountSlotMin">Slot minimum</label>
<input id="accountSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-2">
<label class="form-label" for="accountSlotMax">Slot maximum</label>
<input id="accountSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-3">
<label class="form-label" for="accountDirection">Direction canonique</label>
<select id="accountDirection" class="form-select">
<option value="descending" selected>Descending</option>
<option value="ascending">Ascending</option>
</select>
</div>
</div>
</div>
</div>
<div id="accountsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive app-store-table-container">
<table id="rawAccountsTable" class="table table-hover align-middle mb-0 app-store-table">
<caption class="visually-hidden">Structure future de la table RAW Accounts.</caption>
<thead><tr><th>Pubkey</th><th>Slot</th><th>State hash</th><th>Owner</th><th>Lamports</th><th>Data length</th></tr></thead>
<caption class="visually-hidden">Inspection server-side des états RAW Account.</caption>
<thead>
<tr>
<th>Pubkey</th>
<th>Slot</th>
<th>State hash</th>
<th>Owner</th>
<th>Lamports</th>
<th>Executable</th>
<th>Rent epoch</th>
<th>Data length</th>
<th>Détail</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
@@ -211,6 +254,41 @@
</div>
</div>
<div id="accountDetailModal" class="modal fade" tabindex="-1" aria-labelledby="accountDetailTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 id="accountDetailTitle" class="modal-title h5">RAW Account detail</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div id="accountDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<dl class="row app-runtime-list mb-4">
<dt class="col-sm-3">Pubkey</dt><dd id="accountDetailPubkey" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Slot</dt><dd id="accountDetailSlot" class="col-sm-9"></dd>
<dt class="col-sm-3">State hash</dt><dd id="accountDetailStateHash" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Owner</dt><dd id="accountDetailOwner" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Lamports</dt><dd id="accountDetailLamports" class="col-sm-9"></dd>
<dt class="col-sm-3">Executable</dt><dd id="accountDetailExecutable" class="col-sm-9"></dd>
<dt class="col-sm-3">Rent epoch</dt><dd id="accountDetailRentEpoch" class="col-sm-9"></dd>
<dt class="col-sm-3">Data length</dt><dd id="accountDetailDataLength" class="col-sm-9"></dd>
</dl>
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
<h3 class="h6 mb-0">Account data preview hex</h3>
<div class="d-flex align-items-center gap-2">
<button id="accountDetailDataPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
<i class="fa-regular fa-copy" aria-hidden="true"></i>
<span>Copier</span>
</button>
<span id="accountDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
</div>
</div>
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0"></pre>
</div>
</div>
</div>
</div>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>

View File

@@ -1,11 +1,16 @@
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
// version: 6
// version: 7
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 { StoreTransactionDetailDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts";
@@ -332,23 +337,128 @@ function initializeTransactionTable(): void {
frontendTrace("main", "Store Desk transaction DataTable initialized", { pagingOwner: "datatables", serverSide: true });
}
function initializeAccountSkeleton(): void {
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,
data: [],
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: false,
serverSide: true,
scrollX: true,
language: {
emptyTable: "Aucune donnée RAW Account n'est chargée avant la tranche pre.008.",
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;
frontendTrace("main", "Store Desk account DataTable skeleton initialized", { pagingOwner: "datatables", serverSide: false });
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 renderShellStatus(status: ShellStatusDto): void {
@@ -414,6 +524,83 @@ async function refreshDiagnostics(): Promise<void> {
}
}
function clearAccountDetail(): void {
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);
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 {
for (const elementId of [
"transactionDetailSignature",
@@ -507,6 +694,11 @@ function installInteractions(): void {
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", () => {
@@ -514,6 +706,13 @@ function installInteractions(): void {
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");
@@ -533,6 +732,16 @@ function installInteractions(): void {
}
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;
@@ -545,7 +754,7 @@ function installInteractions(): void {
if (!control) {
return;
}
if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions")) {
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";
@@ -570,6 +779,11 @@ function installInteractions(): void {
controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text",
});
});
const accountDetailModal = document.querySelector<HTMLElement>("#accountDetailModal");
accountDetailModal?.addEventListener("hidden.bs.modal", () => {
clearAccountDetail();
frontendDebug("main", "Store Desk account detail closed");
});
const detailModal = document.querySelector<HTMLElement>("#transactionDetailModal");
detailModal?.addEventListener("hidden.bs.modal", () => {
clearTransactionDetail();
@@ -581,7 +795,7 @@ function installInteractions(): void {
async function initializeMain(): Promise<void> {
frontendInfo("main", "Store Desk main frontend loaded");
initializeTransactionTable();
initializeAccountSkeleton();
initializeAccountTable();
installInteractions();
activateView("overview");
await refreshDiagnostics();