v0.3.8-pre.010

This commit is contained in:
2026-09-03 21:36:34 +02:00
parent 01502678bf
commit e2e603ca6d
15 changed files with 904 additions and 35 deletions

View File

@@ -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 dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source nest 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 dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account nest 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>

View File

@@ -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();