v0.3.8-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 457
|
||||
# version: 458
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.8-pre.6.fix.1"
|
||||
version = "0.3.8-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -74,14 +74,53 @@
|
||||
</section>
|
||||
|
||||
<section data-view-panel="transactions" hidden>
|
||||
<div class="mb-4">
|
||||
<h1 class="h3 mb-1">RAW Transactions</h1>
|
||||
<p class="text-body-secondary mb-0">Structure DataTables conservée hors connexion métier jusqu'à pre.007. Le runtime Store est ouvert par le composite.</p>
|
||||
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">RAW Transactions</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="refreshTransactions" 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-md-4">
|
||||
<label class="form-label" for="transactionSlotMin">Slot minimum</label>
|
||||
<input id="transactionSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="transactionSlotMax">Slot maximum</label>
|
||||
<input id="transactionSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="transactionDirection">Direction canonique</label>
|
||||
<select id="transactionDirection" class="form-select">
|
||||
<option value="descending" selected>Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactionsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive app-store-table-container">
|
||||
<table id="rawTransactionsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<caption class="visually-hidden">Structure future de la table RAW Transactions.</caption>
|
||||
<thead><tr><th>Signature</th><th>Slot</th><th>Block time</th><th>Format</th><th>Payload size</th><th>Retention</th></tr></thead>
|
||||
<caption class="visually-hidden">Inspection server-side des transactions RAW.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Signature</th>
|
||||
<th>Slot</th>
|
||||
<th>Block time</th>
|
||||
<th>Format ID</th>
|
||||
<th>Version</th>
|
||||
<th>Payload size</th>
|
||||
<th>Content hash</th>
|
||||
<th>Retention</th>
|
||||
<th>Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -138,6 +177,34 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="transactionDetailModal" class="modal fade" tabindex="-1" aria-labelledby="transactionDetailTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="transactionDetailTitle" class="modal-title h5">RAW Transaction detail</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="transactionDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<dl class="row app-runtime-list mb-4">
|
||||
<dt class="col-sm-3">Signature</dt><dd id="transactionDetailSignature" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Slot</dt><dd id="transactionDetailSlot" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Block time</dt><dd id="transactionDetailBlockTime" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Format</dt><dd id="transactionDetailFormat" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Content hash</dt><dd id="transactionDetailContentHash" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Retention</dt><dd id="transactionDetailRetention" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Payload size</dt><dd id="transactionDetailPayloadSize" 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">Payload preview hex</h3>
|
||||
<span id="transactionDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
|
||||
</div>
|
||||
<pre id="transactionDetailPayloadPreview" 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>© 2026 SASEDEV</small>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
import DataTable from "datatables.net-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
import "bootstrap";
|
||||
import { Modal } from "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
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";
|
||||
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";
|
||||
|
||||
@@ -16,6 +21,25 @@ 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",
|
||||
@@ -23,12 +47,8 @@ const viewTitles: Record<ViewId, string> = {
|
||||
diagnostics: "Diagnostics",
|
||||
};
|
||||
|
||||
interface EmptyDataTable {
|
||||
columns: { adjust(): void };
|
||||
}
|
||||
|
||||
let transactionTable: EmptyDataTable | null = null;
|
||||
let accountTable: EmptyDataTable | null = null;
|
||||
let transactionTable: StoreDataTable | null = null;
|
||||
let accountTable: StoreDataTable | null = null;
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics";
|
||||
@@ -41,6 +61,13 @@ function setText(elementId: string, value: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -71,25 +98,160 @@ function activateView(viewId: ViewId): void {
|
||||
frontendDebug("main", "Store Desk view activated", { viewId });
|
||||
}
|
||||
|
||||
function initializeEmptyTables(): void {
|
||||
const commonOptions = {
|
||||
data: [],
|
||||
paging: true,
|
||||
serverSide: false,
|
||||
pageLength: 25,
|
||||
lengthMenu: [25, 50, 100],
|
||||
searching: false,
|
||||
ordering: false,
|
||||
info: true,
|
||||
scrollX: true,
|
||||
autoWidth: false,
|
||||
language: {
|
||||
emptyTable: "Aucune donnée RAW n'est chargée dans la tranche pre.006.",
|
||||
},
|
||||
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 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,
|
||||
};
|
||||
transactionTable = new DataTable("#rawTransactionsTable", commonOptions) as unknown as EmptyDataTable;
|
||||
accountTable = new DataTable("#rawAccountsTable", commonOptions) as unknown as EmptyDataTable;
|
||||
frontendTrace("main", "Store Desk empty DataTables skeletons initialized", { pagingOwner: "datatables", serverSide: false });
|
||||
}
|
||||
|
||||
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: [
|
||||
{ className: "font-monospace", data: "signature" },
|
||||
{ className: "font-monospace", data: "slotDecimal" },
|
||||
{ data: "blockTimeUnixMillisDecimal", defaultContent: "—" },
|
||||
{ data: "formatId" },
|
||||
{ data: "formatVersion" },
|
||||
{ data: "payloadSizeDecimal", defaultContent: "—" },
|
||||
{ className: "font-monospace", data: "contentHash" },
|
||||
{ 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="${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 initializeAccountSkeleton(): void {
|
||||
accountTable = new DataTable("#rawAccountsTable", {
|
||||
autoWidth: false,
|
||||
data: [],
|
||||
info: true,
|
||||
lengthMenu: [25, 50, 100],
|
||||
ordering: false,
|
||||
pageLength: 25,
|
||||
paging: true,
|
||||
searching: false,
|
||||
serverSide: false,
|
||||
scrollX: true,
|
||||
language: {
|
||||
emptyTable: "Aucune donnée RAW Account n'est chargée avant la tranche pre.008.",
|
||||
},
|
||||
}) as unknown as StoreDataTable;
|
||||
frontendTrace("main", "Store Desk account DataTable skeleton initialized", { pagingOwner: "datatables", serverSide: false });
|
||||
}
|
||||
|
||||
function renderShellStatus(status: ShellStatusDto): void {
|
||||
@@ -111,21 +273,18 @@ function renderStoreRuntimeStatus(status: StoreRuntimeStatusDto): void {
|
||||
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,
|
||||
@@ -158,6 +317,62 @@ async function refreshDiagnostics(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function clearTransactionDetail(): void {
|
||||
for (const elementId of [
|
||||
"transactionDetailSignature",
|
||||
"transactionDetailSlot",
|
||||
"transactionDetailBlockTime",
|
||||
"transactionDetailFormat",
|
||||
"transactionDetailContentHash",
|
||||
"transactionDetailRetention",
|
||||
"transactionDetailPayloadSize",
|
||||
"transactionDetailPayloadPreview",
|
||||
]) {
|
||||
setText(elementId, "—");
|
||||
}
|
||||
setVisible("transactionDetailError", false);
|
||||
setVisible("transactionDetailPreviewTruncated", false);
|
||||
}
|
||||
|
||||
function renderTransactionDetail(detail: StoreTransactionDetailDto): void {
|
||||
setText("transactionDetailSignature", detail.signature);
|
||||
setText("transactionDetailSlot", detail.slotDecimal);
|
||||
setText("transactionDetailBlockTime", detail.blockTimeUnixMillisDecimal ?? "—");
|
||||
setText("transactionDetailFormat", `${detail.formatId} v${detail.formatVersion}`);
|
||||
setText("transactionDetailContentHash", detail.contentHash);
|
||||
setText("transactionDetailRetention", detail.retentionState);
|
||||
setText("transactionDetailPayloadSize", detail.payloadSizeDecimal ?? "—");
|
||||
setText("transactionDetailPayloadPreview", detail.payloadPreviewHex ?? "Payload non disponible dans cet état de rétention.");
|
||||
setVisible("transactionDetailPreviewTruncated", detail.payloadPreviewTruncated);
|
||||
}
|
||||
|
||||
async function openTransactionDetail(signature: string): Promise<void> {
|
||||
const modalElement = document.querySelector<HTMLElement>("#transactionDetailModal");
|
||||
if (!modalElement) {
|
||||
return;
|
||||
}
|
||||
clearTransactionDetail();
|
||||
setText("transactionDetailSignature", 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);
|
||||
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", () => {
|
||||
@@ -173,6 +388,18 @@ function installInteractions(): void {
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
}
|
||||
const refreshButton = document.querySelector<HTMLButtonElement>("#refreshDiagnostics");
|
||||
refreshButton?.addEventListener("click", () => {
|
||||
frontendDebug("main", "Store Desk diagnostics refresh button clicked");
|
||||
@@ -183,11 +410,19 @@ function installInteractions(): void {
|
||||
if (!(eventTarget instanceof Element)) {
|
||||
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")) {
|
||||
if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions")) {
|
||||
return;
|
||||
}
|
||||
const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous";
|
||||
@@ -212,12 +447,18 @@ function installInteractions(): void {
|
||||
controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text",
|
||||
});
|
||||
});
|
||||
frontendTrace("main", "Store Desk frontend interactions installed", { buttons: true, controls: true, tabs: true });
|
||||
const detailModal = document.querySelector<HTMLElement>("#transactionDetailModal");
|
||||
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");
|
||||
initializeEmptyTables();
|
||||
initializeTransactionTable();
|
||||
initializeAccountSkeleton();
|
||||
installInteractions();
|
||||
activateView("overview");
|
||||
await refreshDiagnostics();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/app_state.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Shared backend state owned by the Store Desk Tauri application.
|
||||
|
||||
@@ -105,7 +105,7 @@ impl crate::AppState {
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.006-store-overview".to_owned(),
|
||||
shell_phase: "pre.007-raw-transactions".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
@@ -115,6 +115,19 @@ impl crate::AppState {
|
||||
return self.store_startup.status().await;
|
||||
}
|
||||
|
||||
/// Executes one bounded RAW transaction inspection query through the retained Store facade.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
return self.store_startup.query_transactions(request).await;
|
||||
}
|
||||
|
||||
/// Loads one explicit RAW transaction detail through the retained Store facade.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
return self.store_startup.transaction_detail(request).await;
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
|
||||
99
crates/ksp-app-store-desk/src/dto_transaction.rs
Normal file
99
crates/ksp-app-store-desk/src/dto_transaction.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_transaction.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned DTOs for RAW transaction inspection and detail commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreTransactionQueryRequestDto {
|
||||
/// Stable Store sort direction code (`ascending` or `descending`).
|
||||
pub(crate) direction: String,
|
||||
/// Absolute zero-based inspection offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Optional inclusive maximum slot encoded as decimal text.
|
||||
pub(crate) slot_max: std::option::Option<String>,
|
||||
/// Optional inclusive minimum slot encoded as decimal text.
|
||||
pub(crate) slot_min: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Payload-free RAW transaction row returned to the server-side DataTables adapter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionRowDto.ts")]
|
||||
pub(crate) struct StoreTransactionRowDto {
|
||||
/// Optional canonical block timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) block_time_unix_millis_decimal: std::option::Option<String>,
|
||||
/// Canonical transaction content hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) content_hash: String,
|
||||
/// KSP-owned RAW format identifier.
|
||||
pub(crate) format_id: String,
|
||||
/// KSP-owned RAW format version, safely representable in JavaScript.
|
||||
pub(crate) format_version: u32,
|
||||
/// Logical payload size encoded as exact decimal text while payload remains retained.
|
||||
pub(crate) payload_size_decimal: std::option::Option<String>,
|
||||
/// Stable logical retention-state code.
|
||||
pub(crate) retention_state: String,
|
||||
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
}
|
||||
|
||||
/// Server-side transaction table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreTransactionQueryResponseDto {
|
||||
/// Exact count after optional Store filters.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before optional filters.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Payload-free inspection rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreTransactionRowDto>,
|
||||
}
|
||||
|
||||
/// App-owned identity request for one explicit RAW transaction detail load.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailRequestDto.ts")]
|
||||
pub(crate) struct StoreTransactionDetailRequestDto {
|
||||
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
}
|
||||
|
||||
/// Bounded detail projection for one RAW transaction.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts")]
|
||||
pub(crate) struct StoreTransactionDetailDto {
|
||||
/// Optional canonical block timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) block_time_unix_millis_decimal: std::option::Option<String>,
|
||||
/// Canonical content hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) content_hash: String,
|
||||
/// KSP-owned RAW format identifier.
|
||||
pub(crate) format_id: String,
|
||||
/// KSP-owned RAW format version.
|
||||
pub(crate) format_version: u32,
|
||||
/// Bounded lower-case hexadecimal preview of the canonical RAW payload, when retained.
|
||||
pub(crate) payload_preview_hex: std::option::Option<String>,
|
||||
/// Whether the canonical payload is larger than the returned preview.
|
||||
pub(crate) payload_preview_truncated: bool,
|
||||
/// Exact canonical payload size while retained.
|
||||
pub(crate) payload_size_decimal: std::option::Option<String>,
|
||||
/// Stable logical retention-state code.
|
||||
pub(crate) retention_state: String,
|
||||
/// Canonical transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_transaction.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/errors.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Application-local error codes for Store Desk desktop runtime surfaces.
|
||||
|
||||
@@ -19,8 +19,14 @@ pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_setting_invalid");
|
||||
/// Store Desk received an invalid transaction inspection/detail request.
|
||||
pub(crate) const ERROR_CODE_STORE_QUERY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_query_invalid");
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
pub(crate) const ERROR_CODE_STORE_RUNTIME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_runtime_unavailable");
|
||||
/// Store Desk cannot complete the bounded Store shutdown lifecycle.
|
||||
pub(crate) const ERROR_CODE_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_shutdown_failed");
|
||||
/// Store Desk could not resolve the requested transaction identity to a coherent Store entity.
|
||||
pub(crate) const ERROR_CODE_STORE_TRANSACTION_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_transaction_not_found");
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "tauri_runtime_failed");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Tauri desktop scaffold for backend-neutral KSP Store inspection.
|
||||
|
||||
@@ -11,6 +11,7 @@ mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_common;
|
||||
mod dto_transaction;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod logging_runtime;
|
||||
@@ -65,6 +66,16 @@ pub(crate) use self::dto_common::CommandErrorDto;
|
||||
pub(crate) use self::dto_common::ShellStatusDto;
|
||||
/// Backend-neutral Store runtime and health projection exposed to Overview.
|
||||
pub(crate) use self::dto_common::StoreRuntimeStatusDto;
|
||||
/// Bounded detail projection for one RAW transaction.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionDetailDto;
|
||||
/// App-owned identity request for one explicit RAW transaction detail load.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionDetailRequestDto;
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionQueryRequestDto;
|
||||
/// Server-side transaction table result using exact decimal counts.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionQueryResponseDto;
|
||||
/// Payload-free RAW transaction row returned to the server-side DataTables adapter.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionRowDto;
|
||||
/// Shared Store Desk runtime state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Store Desk runtime state cannot be locked safely.
|
||||
@@ -81,8 +92,14 @@ pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Store Desk received an invalid transaction inspection/detail request.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_QUERY_INVALID;
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_RUNTIME_UNAVAILABLE;
|
||||
/// Store Desk cannot complete the bounded Store shutdown lifecycle.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_SHUTDOWN_FAILED;
|
||||
/// Store Desk could not resolve the requested transaction identity to a coherent Store entity.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_TRANSACTION_NOT_FOUND;
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/store_runtime.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Composite-selected Store readiness, status and bounded shutdown lifecycle owned by Store Desk.
|
||||
|
||||
@@ -35,6 +35,27 @@ impl crate::StoreStartup {
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded server-side RAW transaction inspection query.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_transactions(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads one explicit bounded RAW transaction detail projection.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.transaction_detail(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
@@ -95,6 +116,68 @@ impl crate::StoreRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one validated random-access transaction inspection query while retaining the Store read guard.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = transaction_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawTransactionInspectionRead::inspect_raw_transactions(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(transaction_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreTransactionQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Loads one transaction detail by application-owned row identity without exposing Store network selection to the frontend.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let reference = transaction_reference_from_request(store, request.signature.as_str());
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let retention = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_retention_state(store, &reference).await;
|
||||
let retention = match retention {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_transaction_not_found()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match retention {
|
||||
ksp_store_lib::RawRetentionState::Full | ksp_store_lib::RawRetentionState::Archived => {
|
||||
transaction_detail_with_payload(store, &reference, retention).await
|
||||
},
|
||||
ksp_store_lib::RawRetentionState::Purged => transaction_detail_from_tombstone(store, &reference).await,
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk cannot project an unsupported transaction retention state",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Explicitly closes the Store exactly once through its backend-neutral facade after all read guards have drained.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let mut locked = self.store.write().await;
|
||||
@@ -127,6 +210,206 @@ impl crate::StoreRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;
|
||||
|
||||
fn transaction_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionInspectionQuery> {
|
||||
let limit = match request.limit {
|
||||
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(request.limit)),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let limit = match limit {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let offset = request.offset.parse::<u64>();
|
||||
let offset = match offset {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
||||
let slot_min = match slot_min {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
||||
let slot_max = match slot_max {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
||||
let slots = match slots {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let direction = match request.direction.as_str() {
|
||||
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
||||
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
let page = ksp_store_lib::RawInspectionPageRequest::new(offset, limit);
|
||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionInspectionQuery::new(network, slots, direction, page));
|
||||
}
|
||||
|
||||
fn transaction_reference_from_request(store: &ksp_store_lib::Store, signature_text: &str) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
||||
let signature = decode_hex_64(signature_text);
|
||||
let signature = match signature {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionReference::new(network, ksp_store_lib::RawTransactionSignature::new(signature)));
|
||||
}
|
||||
|
||||
async fn transaction_detail_with_payload(
|
||||
store: &ksp_store_lib::Store,
|
||||
reference: &ksp_store_lib::RawTransactionReference,
|
||||
retention: ksp_store_lib::RawRetentionState,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let transaction = ksp_store_lib::RawTransactionRead::get_raw_transaction(store, reference).await;
|
||||
let transaction = match transaction {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk transaction retention state and payload availability disagree",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payload = transaction.payload();
|
||||
let payload_bytes = payload.bytes();
|
||||
let preview_len = std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES);
|
||||
let preview = encode_hex(&payload_bytes[..preview_len]);
|
||||
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: transaction.block_time().map(|value| return value.unix_millis().to_string()),
|
||||
content_hash: encode_hex(payload.content_hash().as_bytes()),
|
||||
format_id: payload.format_id().as_str().to_owned(),
|
||||
format_version: payload.format_version(),
|
||||
payload_preview_hex: std::option::Option::Some(preview),
|
||||
payload_preview_truncated: payload_bytes.len() > preview_len,
|
||||
payload_size_decimal: std::option::Option::Some(payload.byte_len().to_string()),
|
||||
retention_state: retention_state_code(retention).to_owned(),
|
||||
signature: encode_hex(reference.signature().as_bytes()),
|
||||
slot_decimal: transaction.slot().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn transaction_detail_from_tombstone(
|
||||
store: &ksp_store_lib::Store,
|
||||
reference: &ksp_store_lib::RawTransactionReference,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let tombstone = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_tombstone(store, reference).await;
|
||||
let tombstone = match tombstone {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk purged transaction is missing its durable tombstone",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::None,
|
||||
content_hash: encode_hex(tombstone.content_hash().as_bytes()),
|
||||
format_id: tombstone.format_id().as_str().to_owned(),
|
||||
format_version: tombstone.format_version(),
|
||||
payload_preview_hex: std::option::Option::None,
|
||||
payload_preview_truncated: false,
|
||||
payload_size_decimal: std::option::Option::None,
|
||||
retention_state: retention_state_code(ksp_store_lib::RawRetentionState::Purged).to_owned(),
|
||||
signature: encode_hex(reference.signature().as_bytes()),
|
||||
slot_decimal: tombstone.slot().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
fn transaction_row_dto(summary: &ksp_store_lib::RawTransactionSummary) -> crate::StoreTransactionRowDto {
|
||||
return crate::StoreTransactionRowDto {
|
||||
block_time_unix_millis_decimal: summary.block_time().map(|value| return value.unix_millis().to_string()),
|
||||
content_hash: encode_hex(summary.content_hash().as_bytes()),
|
||||
format_id: summary.format_id().as_str().to_owned(),
|
||||
format_version: summary.format_version(),
|
||||
payload_size_decimal: summary.payload_size_bytes().map(|value| return value.to_string()),
|
||||
retention_state: retention_state_code(summary.retention_state()).to_owned(),
|
||||
signature: encode_hex(summary.reference().signature().as_bytes()),
|
||||
slot_decimal: summary.slot().to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn retention_state_code(state: ksp_store_lib::RawRetentionState) -> &'static str {
|
||||
return match state {
|
||||
ksp_store_lib::RawRetentionState::Full => "full",
|
||||
ksp_store_lib::RawRetentionState::Compacted => "compacted",
|
||||
ksp_store_lib::RawRetentionState::Archived => "archived",
|
||||
ksp_store_lib::RawRetentionState::Purged => "purged",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_decimal_u64(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => match text.parse::<u64>() {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_hex_64(value: &str) -> ksp_core_lib::Result<[u8; 64]> {
|
||||
if value.len() != 128 || !value.is_ascii() {
|
||||
return std::result::Result::Err(store_query_invalid());
|
||||
}
|
||||
let source = value.as_bytes();
|
||||
let mut decoded = [0_u8; 64];
|
||||
for index in 0..64 {
|
||||
let high = decode_hex_nibble(source[index * 2]);
|
||||
let low = decode_hex_nibble(source[index * 2 + 1]);
|
||||
let (high, low) = match (high, low) {
|
||||
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
decoded[index] = (high << 4) | low;
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn decode_hex_nibble(value: u8) -> std::option::Option<u8> {
|
||||
return match value {
|
||||
b'0'..=b'9' => std::option::Option::Some(value - b'0'),
|
||||
b'a'..=b'f' => std::option::Option::Some(value - b'a' + 10),
|
||||
b'A'..=b'F' => std::option::Option::Some(value - b'A' + 10),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn encode_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut encoded = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let value = *byte;
|
||||
encoded.push(char::from(HEX[usize::from(value >> 4)]));
|
||||
encoded.push(char::from(HEX[usize::from(value & 0x0f)]));
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
fn store_query_invalid() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_QUERY_INVALID, "Store Desk transaction query is invalid");
|
||||
}
|
||||
|
||||
fn store_runtime_unavailable() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_RUNTIME_UNAVAILABLE, "Store Desk Store runtime is unavailable");
|
||||
}
|
||||
|
||||
fn store_transaction_not_found() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_TRANSACTION_NOT_FOUND, "Store Desk transaction was not found");
|
||||
}
|
||||
|
||||
/// Resolves the composite Store target, opens Store through the facade and captures one initial readiness probe.
|
||||
pub(crate) async fn initialize_store(management: &ksp_config_lib::ConfigManagement) -> crate::StoreStartup {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/tauri.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Tauri runtime assembly for the KSP Store desktop application.
|
||||
|
||||
@@ -75,7 +75,14 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_shell_status, splash_frontend_ready, store_runtime_status]);
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
emit_frontend_log,
|
||||
get_shell_status,
|
||||
splash_frontend_ready,
|
||||
store_get_transaction_detail,
|
||||
store_query_transactions,
|
||||
store_runtime_status,
|
||||
]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
@@ -167,6 +174,30 @@ fn get_shell_status(state: tauri::State<'_, crate::AppState>) -> std::result::Re
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_transactions(
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreTransactionQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_transactions(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("store_query_transactions", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_get_transaction_detail(
|
||||
request: crate::StoreTransactionDetailRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreTransactionDetailDto, crate::CommandErrorDto> {
|
||||
let result = state.transaction_detail(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("store_get_transaction_detail", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::StoreRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
return std::result::Result::Ok(state.store_runtime_status().await);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Dependency-boundary tests for the Store Desk application.
|
||||
|
||||
@@ -75,3 +75,32 @@ fn pre_006_fix_001_shared_visible_items_use_the_crate_root_facade_inside_owner_m
|
||||
assert!(tw_splash.contains("const WINDOW_LABEL_SPLASH: &str = \"splash\";"));
|
||||
assert!(!tw_splash.contains("pub(crate) const WINDOW_LABEL_SPLASH"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_inspection_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionDetailRequestDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionDetailDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionQueryRequestDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionQueryResponseDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionRowDto;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing pre.007 crate-root DTO re-export: {expected}");
|
||||
}
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionInspectionRead::inspect_raw_transactions"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRead::get_raw_transaction"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_retention_state"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_tombstone"));
|
||||
assert!(app_state.contains("request: crate::StoreTransactionQueryRequestDto"));
|
||||
assert!(app_state.contains("request: crate::StoreTransactionDetailRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreTransactionQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreTransactionDetailRequestDto"));
|
||||
let sources = [runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["ksp_store_api", "ksp_store_postgres", "tokio_postgres", "deadpool_postgres", "SELECT ", "OFFSET "] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk transaction inspection crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Desktop template and frontend contract tests for the Store Desk scaffold.
|
||||
|
||||
@@ -80,7 +80,7 @@ fn pre_002_binary_assets_reuse_the_ksp_desktop_template_byte_exact() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_shell_exposes_planned_views_store_overview_and_unconnected_raw_table_skeletons() {
|
||||
fn pre_007_shell_exposes_transaction_filters_detail_and_keeps_account_skeleton_unconnected() {
|
||||
let html = include_str!("../frontend/main.html");
|
||||
for view in ["overview", "transactions", "accounts", "diagnostics"] {
|
||||
assert!(html.contains(&format!("data-view=\"{view}\"")));
|
||||
@@ -92,18 +92,26 @@ fn pre_006_shell_exposes_planned_views_store_overview_and_unconnected_raw_table_
|
||||
{
|
||||
assert!(html.contains(&format!("id=\"{overview_id}\"")), "missing Store Overview field: {overview_id}");
|
||||
}
|
||||
assert!(html.contains("jusqu'à pre.007"));
|
||||
for transaction_id in ["refreshTransactions", "transactionSlotMin", "transactionSlotMax", "transactionDirection", "transactionDetailModal"] {
|
||||
assert!(html.contains(&format!("id=\"{transaction_id}\"")), "missing pre.007 transaction control: {transaction_id}");
|
||||
}
|
||||
assert!(html.contains("jusqu'à pre.008"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_datatables_remain_unconnected_with_one_pager_owner_and_no_store_pager() {
|
||||
fn pre_007_transaction_datatable_is_server_side_while_account_remains_unconnected_and_no_second_pager_exists() {
|
||||
let source = include_str!("../frontend/ts/main.ts");
|
||||
assert!(source.contains("datatables.net-bs5"));
|
||||
assert!(source.contains("paging: true"));
|
||||
assert!(source.contains("pageLength: 25"));
|
||||
assert!(source.contains("lengthMenu: [25, 50, 100]"));
|
||||
assert!(source.contains("store_query_transactions"));
|
||||
assert!(source.contains("serverSide: true"));
|
||||
assert!(source.contains("Store Desk transaction DataTable initialized"));
|
||||
assert!(source.contains("Store Desk account DataTable skeleton initialized"));
|
||||
assert!(source.contains("serverSide: false"));
|
||||
assert!(source.contains("searching: false"));
|
||||
assert!(source.contains("ordering: false"));
|
||||
let html = include_str!("../frontend/main.html");
|
||||
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
||||
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
||||
@@ -167,3 +175,25 @@ fn pre_006_store_lifecycle_is_composite_managed_health_visible_and_bounded_on_wi
|
||||
assert!(tauri.contains("app_handle.exit(0)"));
|
||||
assert!(tauri.contains("store_runtime_status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_ipc_maps_datatables_offset_limit_counts_and_detail_without_cursor_contract() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
assert!(dto.contains("records_total_decimal"));
|
||||
assert!(dto.contains("records_filtered_decimal"));
|
||||
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
||||
assert!(runtime.contains("RawTransactionInspectionQuery::new"));
|
||||
assert!(tauri.contains("store_query_transactions"));
|
||||
assert!(tauri.contains("store_get_transaction_detail"));
|
||||
assert!(frontend.contains("offset: page.start.toString()"));
|
||||
assert!(frontend.contains("limit: page.length"));
|
||||
assert!(frontend.contains("decimalCountToSafeNumber"));
|
||||
assert!(frontend.contains("recordsTotalDecimal"));
|
||||
assert!(frontend.contains("recordsFilteredDecimal"));
|
||||
assert!(frontend.contains("data-transaction-detail"));
|
||||
assert!(!dto.contains("RawPageCursor"));
|
||||
assert!(!frontend.contains("storePager"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Security boundary tests for the Store Desk scaffold.
|
||||
|
||||
@@ -80,3 +80,51 @@ fn pre_006_store_status_ipc_is_backend_neutral_and_secret_free() {
|
||||
assert!(!dto.contains(forbidden), "Store status DTO source contains forbidden field/material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_rows_are_payload_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let row_start = dto.find("pub(crate) struct StoreTransactionRowDto");
|
||||
let response_start = dto.find("pub(crate) struct StoreTransactionQueryResponseDto");
|
||||
assert!(row_start.is_some());
|
||||
assert!(response_start.is_some());
|
||||
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
||||
let row_source = &dto[row_start..response_start];
|
||||
assert!(!row_source.contains("payload_preview_hex"));
|
||||
assert!(!row_source.contains("payload: "));
|
||||
}
|
||||
assert!(dto.contains("payload_preview_hex"));
|
||||
assert!(dto.contains("payload_preview_truncated"));
|
||||
assert!(runtime.contains("TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512"));
|
||||
assert!(runtime.contains("std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES)"));
|
||||
assert!(frontend.contains("transaction DataTables server-side query started"));
|
||||
assert!(frontend.contains("transaction DataTables page changed"));
|
||||
assert!(frontend.contains("transaction DataTables page length changed"));
|
||||
assert!(frontend.contains("transaction DataTables redraw completed"));
|
||||
assert!(frontend.contains("transaction detail opened"));
|
||||
assert!(frontend.contains("transaction detail closed"));
|
||||
for forbidden in [
|
||||
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMin",
|
||||
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMax",
|
||||
"frontendDebug(\"main\", \"Store Desk transaction detail opened\", { signature",
|
||||
"payloadPreviewHex:",
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "transaction business/filter value leaked into frontend tracing: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_request_rejects_datatables_search_order_draw_and_physical_material() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "connection_uri", "postgresql://", "endpoint"] {
|
||||
assert!(!dto.contains(forbidden), "forbidden DataTables/physical request material present: {forbidden}");
|
||||
}
|
||||
assert!(runtime.contains("25 | 50 | 100"));
|
||||
assert!(runtime.contains("request.offset.parse::<u64>()"));
|
||||
assert!(runtime.contains("RawSlotRange::new"));
|
||||
assert!(runtime.contains("\"ascending\" =>"));
|
||||
assert!(runtime.contains("\"descending\" =>"));
|
||||
}
|
||||
|
||||
56
crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
Normal file
56
crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_rows_and_detail_keep_raw_bytes_out_of_table_contract() {
|
||||
let row = crate::StoreTransactionRowDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::Some("1720000000000".to_owned()),
|
||||
content_hash: "11".repeat(32),
|
||||
format_id: "ksp_raw_tx_v1".to_owned(),
|
||||
format_version: 1,
|
||||
payload_size_decimal: std::option::Option::Some("4096".to_owned()),
|
||||
retention_state: "full".to_owned(),
|
||||
signature: "22".repeat(64),
|
||||
slot_decimal: "123456789".to_owned(),
|
||||
};
|
||||
let serialized = serde_json::to_value(&row);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serialized.get("payloadPreviewHex").is_none());
|
||||
assert!(serialized.get("payload").is_none());
|
||||
assert_eq!(serialized["payloadSizeDecimal"], "4096");
|
||||
let detail = crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::None,
|
||||
content_hash: "33".repeat(32),
|
||||
format_id: "ksp_raw_tx_v1".to_owned(),
|
||||
format_version: 1,
|
||||
payload_preview_hex: std::option::Option::Some("abcd".to_owned()),
|
||||
payload_preview_truncated: true,
|
||||
payload_size_decimal: std::option::Option::Some("8192".to_owned()),
|
||||
retention_state: "archived".to_owned(),
|
||||
signature: "44".repeat(64),
|
||||
slot_decimal: "987654321".to_owned(),
|
||||
};
|
||||
let serialized = serde_json::to_value(&detail);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(serialized["payloadPreviewHex"], "abcd");
|
||||
assert_eq!(serialized["payloadPreviewTruncated"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_query_request_contains_only_random_access_and_explicit_store_filters() {
|
||||
let source = include_str!("../src/dto_transaction.rs");
|
||||
for required in ["direction", "offset", "limit", "slot_max", "slot_min"] {
|
||||
assert!(source.contains(required), "missing Store transaction query field: {required}");
|
||||
}
|
||||
for forbidden in ["draw", "columns", "search", "order", "cursor", "connection_uri", "endpoint"] {
|
||||
assert!(!source.contains(&format!("pub(crate) {forbidden}")), "DataTables/physical field leaked into Store transaction query DTO: {forbidden}");
|
||||
}
|
||||
}
|
||||
122
deltas/0.3.8/pre.007.md
Normal file
122
deltas/0.3.8/pre.007.md
Normal file
@@ -0,0 +1,122 @@
|
||||
<!-- file: deltas/0.3.8/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.8-pre.007` — RAW Transactions DataTables server-side + détail
|
||||
|
||||
## Base requise
|
||||
|
||||
Base directe attendue : `0.3.8-pre.006-fix.001` (`workspace.package.version = 0.3.8-pre.6.fix.1`).
|
||||
|
||||
## Objet
|
||||
|
||||
Brancher la vue `RAW Transactions` de `ksp-app-store-desk` sur la capability d'inspection Store introduite en `pre.003`/`pre.004`, sans modifier Store API, PostgreSQL ou les queries keyset machine.
|
||||
|
||||
## Backend applicatif
|
||||
|
||||
Store Desk ajoute des DTO app-owned et deux commands Tauri :
|
||||
|
||||
```text
|
||||
store_query_transactions
|
||||
store_get_transaction_detail
|
||||
```
|
||||
|
||||
`store_query_transactions` reçoit uniquement :
|
||||
|
||||
```text
|
||||
offset decimal
|
||||
limit = 25 | 50 | 100
|
||||
slot_min optionnel
|
||||
slot_max optionnel
|
||||
direction = ascending | descending
|
||||
```
|
||||
|
||||
Le network n'est jamais fourni par le frontend : il est dérivé du `ksp-store-lib::Store` déjà ouvert. Le runtime construit `RawInspectionPageRequest` + `RawTransactionInspectionQuery` puis appelle `RawTransactionInspectionRead`.
|
||||
|
||||
La réponse app-owned contient :
|
||||
|
||||
```text
|
||||
rows payload-free
|
||||
records_total_decimal
|
||||
records_filtered_decimal
|
||||
```
|
||||
|
||||
Tous les `u64` qui peuvent dépasser le domaine JS-safe restent encodés en texte décimal.
|
||||
|
||||
## Table Transactions
|
||||
|
||||
La table passe à :
|
||||
|
||||
```text
|
||||
serverSide: true
|
||||
searching: false
|
||||
ordering: false
|
||||
pageLength: 25
|
||||
lengthMenu: [25, 50, 100]
|
||||
ajax: fonction custom -> invoke Tauri
|
||||
```
|
||||
|
||||
Le mapping visible est :
|
||||
|
||||
```text
|
||||
DataTables start -> offset
|
||||
DataTables length -> limit
|
||||
Store total -> recordsTotal
|
||||
Store filtered -> recordsFiltered
|
||||
Store summaries -> data
|
||||
DataTables draw -> frontend uniquement
|
||||
```
|
||||
|
||||
Les filtres Store explicites sont `slot min`, `slot max` et `direction`. Tout changement de filtre redessine depuis la première page. Aucun pager Store ou cursor n'est rendu.
|
||||
|
||||
## Rows et détail
|
||||
|
||||
Les rows exposent uniquement signature encodée, slot, block time, format ID/version, payload size, content hash et retention state. Aucun payload byte n'est présent dans la table.
|
||||
|
||||
Le bouton `Détail` charge une seule identité :
|
||||
|
||||
- `Full` / `Archived` : `RawTransactionRead` fournit le payload logique ; Store Desk n'en expose qu'un preview hex des 512 premiers bytes maximum ;
|
||||
- `Purged` : `RawTransactionRetentionRead` fournit le tombstone ; aucun block time/payload n'est inventé ;
|
||||
- la taille exacte et le flag de troncature sont séparés du preview ;
|
||||
- aucun export exact 16 MiB n'est ajouté.
|
||||
|
||||
## Sécurité / instrumentation
|
||||
|
||||
- rejet Rust de `length=-1` et de toute taille autre que 25/50/100 ;
|
||||
- offset, slots et signature parsés sans écho de valeur dans les erreurs ;
|
||||
- counts décimaux convertis côté TypeScript seulement après contrôle `Number.MAX_SAFE_INTEGER` ;
|
||||
- aucune search/order DataTables arbitraire dans le contrat IPC ;
|
||||
- aucune URI, credential, SQL, handle PostgreSQL ou payload dans les logs ;
|
||||
- tracing query/page/page-length/redraw/refresh/detail sans valeurs de filtres ni identité ;
|
||||
- nouveaux items `pub(crate)` réexportés par `lib.rs` et consommés via `crate::Item` conformément à `RUST-IMPORT-009/012` et `RUST-API-004`.
|
||||
|
||||
## Hors scope
|
||||
|
||||
`RAW Accounts` reste local et `serverSide:false` jusqu'à `pre.008`. Aucun changement de Store API, Store façade, PostgreSQL, migration, index, capability ou Config composite.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
delivery = 0.3.8-pre.007
|
||||
workspace.package.version = 0.3.8-pre.7
|
||||
commit = v0.3.8-pre.007
|
||||
tag = aucun
|
||||
```
|
||||
|
||||
## Gate requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-store-desk
|
||||
cargo test -p ksp-store-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Smoke runtime recommandé :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri dev)
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Plan v0.3.8 — Store Desk V1 RAW
|
||||
|
||||
@@ -586,6 +586,8 @@ Le gate opérateur de `pre.006` est fonctionnellement vert mais signale un warni
|
||||
|
||||
Brancher l'unique pager DataTables via `ajax` function/Tauri invoke, mapping offset/limit/totaux, filtres Store explicites, summary rows, retention badges, détail à la demande et instrumentation. Aucun second pager.
|
||||
|
||||
Implémentation retenue : `store_query_transactions` reçoit uniquement `offset`, `limit`, `slot_min`, `slot_max` et `direction`, dérive le network du Store déjà ouvert et appelle `RawTransactionInspectionRead`. Les counts restent des chaînes décimales jusqu'au contrôle `Number.MAX_SAFE_INTEGER` côté frontend. Les rows sont payload-free. `store_get_transaction_detail` reçoit uniquement la signature de la row, recharge l'entité via `RawTransactionRead`/`RawTransactionRetentionRead` et expose au maximum 512 bytes sous forme de preview hex, avec taille exacte et indicateur de troncature. `Full`, `Archived` et `Purged` sont distingués ; un `Purged` utilise son tombstone. Les Accounts restent `serverSide:false` jusqu'à `pre.008`.
|
||||
|
||||
### pre.008 — RawAccountState DataTables serverSide + détail
|
||||
|
||||
Même intégration pour Account, pubkey/range/direction, summaries JS-safe, détail data borné, responsive/scrollX/copy UX.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Validation v0.3.8 — Store Desk V1 RAW
|
||||
|
||||
@@ -538,3 +538,43 @@ Le réexport `StoreRuntime` est requis par la règle de façade crate-root et ne
|
||||
- [X] un canari `dependency_boundary` verrouille ces chemins et la distinction item partagé / helper local privé.
|
||||
|
||||
Aucun composite Config, Store runtime, query, SQL, DTO wire, commande Tauri, frontend, DataTables ou comportement de fermeture n'est modifié.
|
||||
## 28. Clôture opérateur `pre.006-fix.001` et `pre.007` — RAW Transactions server-side
|
||||
|
||||
Le gate opérateur de `0.3.8-pre.6.fix.1` est entièrement vert et ne contient plus le warning de réexport `StoreRuntime` :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (314 table(s), 703 file(s))
|
||||
cargo check --workspace: PASS
|
||||
cargo clippy --workspace --all-targets: PASS
|
||||
cargo test -p ksp-app-store-desk: PASS (14 unitaires + 3 dependency + 8 desktop contract + 5 security + 1 public API)
|
||||
cargo test -p ksp-config-lib: PASS (128 unitaires + intégrations)
|
||||
cargo test -p ksp-store-lib: PASS
|
||||
cargo check -p ksp-store-lib --no-default-features: PASS
|
||||
```
|
||||
|
||||
`pre.007` branche uniquement la famille RAW Transactions :
|
||||
|
||||
- [X] `workspace.package.version = 0.3.8-pre.7` ;
|
||||
- [X] `StoreTransactionQueryRequestDto` ne transporte que offset décimal, limit 25/50/100, slot min/max optionnels et direction canonique ;
|
||||
- [X] aucun `draw`, colonnes DataTables, search arbitraire, order arbitraire, cursor Store, network ou donnée physique ne traverse le request IPC ;
|
||||
- [X] `StoreRuntime` dérive le network du Store ouvert et appelle `RawTransactionInspectionRead` via `ksp-store-lib` uniquement ;
|
||||
- [X] `records_total_decimal` et `records_filtered_decimal` restent exacts jusqu'au contrôle JS-safe côté frontend ;
|
||||
- [X] les rows DataTables contiennent signature/slot/block time/format/version/taille/hash/rétention, sans payload bytes ;
|
||||
- [X] Transactions utilise `serverSide:true`, `searching:false`, `ordering:false`, page length 25/50/100 et `ajax` Tauri custom ;
|
||||
- [X] DataTables reste l'unique pager visible ; aucun cursor/pager Store parallèle n'est rendu ;
|
||||
- [X] les filtres explicites slot min/max + direction déclenchent un redraw depuis la première page ;
|
||||
- [X] `length=-1`, taille hors whitelist, offset non décimal, range inversée et direction inconnue sont rejetés avant la query Store ;
|
||||
- [X] les counts hors `Number.MAX_SAFE_INTEGER` produisent une erreur UX au lieu d'un arrondi silencieux ;
|
||||
- [X] `store_get_transaction_detail` reçoit uniquement la signature encodée de la row et ne reçoit jamais le network du frontend ;
|
||||
- [X] le détail `Full`/`Archived` charge une seule transaction et expose au maximum 512 bytes en preview hex ;
|
||||
- [X] le détail `Purged` utilise `RawTransactionTombstone`, sans inventer block time ou payload ;
|
||||
- [X] les événements query/page/page-length/redraw/refresh/détail sont tracés sans valeurs de slot, signature ou payload ;
|
||||
- [X] les nouveaux items `pub(crate)` sont réexportés via `lib.rs` et consommés sous `crate::Item` ;
|
||||
- [X] RAW Accounts reste volontairement `serverSide:false` jusqu'à `pre.008` ;
|
||||
- [X] aucune modification `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, migration ou capability n'est introduite par cette tranche applicative.
|
||||
|
||||
Le gate Cargo opérateur de `pre.007` reste à exécuter après application du delta.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user