v0.3.8-pre.010
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -249,6 +249,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<pre id="transactionDetailPayloadPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0">—</pre>
|
||||
<section class="border-top mt-4 pt-4" aria-labelledby="transactionObservationsTitle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 id="transactionObservationsTitle" class="h6 mb-1">Observations d’acquisition</h3>
|
||||
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source n’est chargé.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactionObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive">
|
||||
<table id="transactionObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,6 +303,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0">—</pre>
|
||||
<section class="border-top mt-4 pt-4" aria-labelledby="accountObservationsTitle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 id="accountObservationsTitle" class="h6 mb-1">Observations d’acquisition</h3>
|
||||
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account n’est chargé.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive">
|
||||
<table id="accountObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th><th>Startup</th><th>Write version</th><th>Tx signature</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
import DataTable from "datatables.net-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
@@ -13,6 +13,12 @@ import type { StoreAccountQueryResponseDto } from "./bindings/ksp_app_store_desk
|
||||
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";
|
||||
@@ -54,6 +60,10 @@ const viewTitles: Record<ViewId, string> = {
|
||||
|
||||
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;
|
||||
@@ -211,6 +221,32 @@ function decimalCountToSafeNumber(value: string): number | 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;
|
||||
@@ -461,6 +497,176 @@ function initializeAccountTable(): void {
|
||||
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);
|
||||
@@ -525,6 +731,9 @@ async function refreshDiagnostics(): Promise<void> {
|
||||
}
|
||||
|
||||
function clearAccountDetail(): void {
|
||||
currentAccountObservationIdentity = null;
|
||||
setAccountObservationError(null);
|
||||
accountObservationTable?.draw(true);
|
||||
for (const elementId of [
|
||||
"accountDetailPubkey",
|
||||
"accountDetailSlot",
|
||||
@@ -588,6 +797,8 @@ async function openAccountDetail(pubkey: string, slot: string, stateHash: string
|
||||
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é.");
|
||||
@@ -602,6 +813,9 @@ function redrawAccounts(resetPaging: boolean, reason: string): void {
|
||||
}
|
||||
|
||||
function clearTransactionDetail(): void {
|
||||
currentTransactionObservationSignature = null;
|
||||
setTransactionObservationError(null);
|
||||
transactionObservationTable?.draw(true);
|
||||
for (const elementId of [
|
||||
"transactionDetailSignature",
|
||||
"transactionDetailSlot",
|
||||
@@ -661,6 +875,8 @@ async function openTransactionDetail(signature: string): Promise<void> {
|
||||
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é.");
|
||||
@@ -780,11 +996,17 @@ function installInteractions(): void {
|
||||
});
|
||||
});
|
||||
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");
|
||||
@@ -796,6 +1018,8 @@ async function initializeMain(): Promise<void> {
|
||||
frontendInfo("main", "Store Desk main frontend loaded");
|
||||
initializeTransactionTable();
|
||||
initializeAccountTable();
|
||||
initializeTransactionObservationTable();
|
||||
initializeAccountObservationTable();
|
||||
installInteractions();
|
||||
activateView("overview");
|
||||
await refreshDiagnostics();
|
||||
|
||||
Reference in New Issue
Block a user