v0.3.8-pre.007

This commit is contained in:
2026-09-03 17:54:50 +02:00
parent 8130e01b70
commit d5b4aef194
16 changed files with 1139 additions and 55 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 457 # version: 458
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.8-pre.6.fix.1" version = "0.3.8-pre.7"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-store-desk/frontend/main.html --> <!-- file: crates/ksp-app-store-desk/frontend/main.html -->
<!-- version: 2 --> <!-- version: 3 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -74,14 +74,53 @@
</section> </section>
<section data-view-panel="transactions" hidden> <section data-view-panel="transactions" hidden>
<div class="mb-4"> <div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
<h1 class="h3 mb-1">RAW Transactions</h1> <div>
<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> <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>
<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"> <div class="table-responsive app-store-table-container">
<table id="rawTransactionsTable" class="table table-hover align-middle mb-0 app-store-table"> <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> <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</th><th>Payload size</th><th>Retention</th></tr></thead> <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> <tbody></tbody>
</table> </table>
</div> </div>
@@ -138,6 +177,34 @@
</div> </div>
</main> </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"> <footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center"> <div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small> <small>&copy; 2026 SASEDEV</small>

View File

@@ -1,13 +1,18 @@
// file: crates/ksp-app-store-desk/frontend/ts/main.ts // file: crates/ksp-app-store-desk/frontend/ts/main.ts
// version: 3 // version: 4
import DataTable from "datatables.net-bs5"; import DataTable from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css"; import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import "bootstrap"; import { Modal } from "bootstrap";
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
import "simplebar"; import "simplebar";
import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.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 { 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 { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
@@ -16,6 +21,25 @@ installFrontendConsoleBridge("main");
type ViewId = "overview" | "transactions" | "accounts" | "diagnostics"; 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> = { const viewTitles: Record<ViewId, string> = {
overview: "Overview", overview: "Overview",
transactions: "RAW Transactions", transactions: "RAW Transactions",
@@ -23,12 +47,8 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics", diagnostics: "Diagnostics",
}; };
interface EmptyDataTable { let transactionTable: StoreDataTable | null = null;
columns: { adjust(): void }; let accountTable: StoreDataTable | null = null;
}
let transactionTable: EmptyDataTable | null = null;
let accountTable: EmptyDataTable | null = null;
function isViewId(value: string | undefined): value is ViewId { function isViewId(value: string | undefined): value is ViewId {
return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics"; 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 { function renderDiagnostic(elementId: string, diagnostic: { domain: string; code: string; message: string } | null): void {
const element = document.querySelector<HTMLElement>(`#${elementId}`); const element = document.querySelector<HTMLElement>(`#${elementId}`);
if (!element) { if (!element) {
@@ -71,25 +98,160 @@ function activateView(viewId: ViewId): void {
frontendDebug("main", "Store Desk view activated", { viewId }); frontendDebug("main", "Store Desk view activated", { viewId });
} }
function initializeEmptyTables(): void { function decimalCountToSafeNumber(value: string): number | null {
const commonOptions = { if (!/^\d+$/.test(value)) {
data: [], return null;
paging: true, }
serverSide: false, const exact = BigInt(value);
pageLength: 25, if (exact > BigInt(Number.MAX_SAFE_INTEGER)) {
lengthMenu: [25, 50, 100], return null;
searching: false, }
ordering: false, return Number(exact);
info: true, }
scrollX: true,
autoWidth: false, function currentTransactionQuery(page: DataTablesPageRequest): StoreTransactionQueryRequestDto | null {
language: { if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) {
emptyTable: "Aucune donnée RAW n'est chargée dans la tranche pre.006.", 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 { 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 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 pool = `${status.poolAvailable}/${status.poolSize}/${status.poolCapacity} · wait ${status.poolWaiting}`;
const profile = status.profileId ?? "indisponible"; const profile = status.profileId ?? "indisponible";
setText("overviewStoreProfile", profile); setText("overviewStoreProfile", profile);
setText("overviewStoreTarget", target); setText("overviewStoreTarget", target);
setText("overviewStoreHealth", status.healthState); setText("overviewStoreHealth", status.healthState);
setText("overviewStorePool", pool); setText("overviewStorePool", pool);
setText("overviewStoreMigration", migration); setText("overviewStoreMigration", migration);
renderDiagnostic("overviewStoreDiagnostic", status.diagnostic); renderDiagnostic("overviewStoreDiagnostic", status.diagnostic);
setText("runtimeStoreProfile", profile); setText("runtimeStoreProfile", profile);
setText("runtimeStoreTarget", target); setText("runtimeStoreTarget", target);
setText("runtimeStoreHealth", status.healthState); setText("runtimeStoreHealth", status.healthState);
setText("runtimeStoreMigration", migration); setText("runtimeStoreMigration", migration);
setText("runtimeStorePool", pool); setText("runtimeStorePool", pool);
renderDiagnostic("runtimeStoreDiagnostic", status.diagnostic); renderDiagnostic("runtimeStoreDiagnostic", status.diagnostic);
frontendTrace("main", "Store Desk Store runtime status rendered", { frontendTrace("main", "Store Desk Store runtime status rendered", {
healthState: status.healthState, healthState: status.healthState,
pendingMigrationCount: status.pendingMigrationCount, 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 { function installInteractions(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => { document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
@@ -173,6 +388,18 @@ function installInteractions(): void {
frontendDebug("main", "Store Desk Overview refresh button clicked"); frontendDebug("main", "Store Desk Overview refresh button clicked");
void refreshStoreRuntime(); 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"); const refreshButton = document.querySelector<HTMLButtonElement>("#refreshDiagnostics");
refreshButton?.addEventListener("click", () => { refreshButton?.addEventListener("click", () => {
frontendDebug("main", "Store Desk diagnostics refresh button clicked"); frontendDebug("main", "Store Desk diagnostics refresh button clicked");
@@ -183,11 +410,19 @@ function installInteractions(): void {
if (!(eventTarget instanceof Element)) { if (!(eventTarget instanceof Element)) {
return; 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']"); const control = eventTarget.closest<HTMLElement>("button, [role='tab'], [data-bs-toggle='tab']");
if (!control) { if (!control) {
return; 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; return;
} }
const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous"; 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", 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> { async function initializeMain(): Promise<void> {
frontendInfo("main", "Store Desk main frontend loaded"); frontendInfo("main", "Store Desk main frontend loaded");
initializeEmptyTables(); initializeTransactionTable();
initializeAccountSkeleton();
installInteractions(); installInteractions();
activateView("overview"); activateView("overview");
await refreshDiagnostics(); await refreshDiagnostics();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/app_state.rs // file: crates/ksp-app-store-desk/src/app_state.rs
// version: 2 // version: 3
//! Shared backend state owned by the Store Desk Tauri application. //! 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(), application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count, config_document_count: document_count,
fallback_logging_active: runtime.fallback_active, 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(), startup_diagnostic: runtime.startup_diagnostic.clone(),
}); });
} }
@@ -115,6 +115,19 @@ impl crate::AppState {
return self.store_startup.status().await; 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. /// Returns the resolved splash timings captured during application bootstrap.
#[must_use] #[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {

View 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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/errors.rs // file: crates/ksp-app-store-desk/src/errors.rs
// version: 2 // version: 3
//! Application-local error codes for Store Desk desktop runtime surfaces. //! 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"); 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. /// 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"); 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. /// 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"); 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. /// 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"); 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. /// A required Tauri window is missing from the configured application runtime.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/lib.rs // file: crates/ksp-app-store-desk/src/lib.rs
// version: 3 // version: 4
//! Tauri desktop scaffold for backend-neutral KSP Store inspection. //! Tauri desktop scaffold for backend-neutral KSP Store inspection.
@@ -11,6 +11,7 @@ mod app_state;
mod bootstrap; mod bootstrap;
mod constants; mod constants;
mod dto_common; mod dto_common;
mod dto_transaction;
mod errors; mod errors;
mod frontend_logging; mod frontend_logging;
mod logging_runtime; mod logging_runtime;
@@ -65,6 +66,16 @@ pub(crate) use self::dto_common::CommandErrorDto;
pub(crate) use self::dto_common::ShellStatusDto; pub(crate) use self::dto_common::ShellStatusDto;
/// Backend-neutral Store runtime and health projection exposed to Overview. /// Backend-neutral Store runtime and health projection exposed to Overview.
pub(crate) use self::dto_common::StoreRuntimeStatusDto; 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. /// Shared Store Desk runtime state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Store Desk runtime state cannot be locked safely. /// 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; pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound. /// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID; 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. /// Store Desk cannot complete the bounded Store shutdown lifecycle.
pub(crate) use self::errors::ERROR_CODE_STORE_SHUTDOWN_FAILED; 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. /// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime. /// A required Tauri window is missing from the configured application runtime.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/store_runtime.rs // 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. //! 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. /// Closes the retained Store runtime if startup reached the physical Store-open phase.
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> { pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let runtime = self.runtime.as_ref(); 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. /// 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<()> { pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let mut locked = self.store.write().await; 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. /// 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 { pub(crate) async fn initialize_store(management: &ksp_config_lib::ConfigManagement) -> crate::StoreStartup {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/tauri.rs // file: crates/ksp-app-store-desk/src/tauri.rs
// version: 2 // version: 3
//! Tauri runtime assembly for the KSP Store desktop application. //! 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. #[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> { 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> { 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] #[tauri::command]
async fn store_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::StoreRuntimeStatusDto, crate::CommandErrorDto> { 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); return std::result::Result::Ok(state.store_runtime_status().await);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs // file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
// version: 4 // version: 5
//! Dependency-boundary tests for the Store Desk application. //! 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("const WINDOW_LABEL_SPLASH: &str = \"splash\";"));
assert!(!tw_splash.contains("pub(crate) const WINDOW_LABEL_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}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs // 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. //! 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] #[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"); let html = include_str!("../frontend/main.html");
for view in ["overview", "transactions", "accounts", "diagnostics"] { for view in ["overview", "transactions", "accounts", "diagnostics"] {
assert!(html.contains(&format!("data-view=\"{view}\""))); 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(&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")); assert!(html.contains("jusqu'à pre.008"));
} }
#[test] #[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"); let source = include_str!("../frontend/ts/main.ts");
assert!(source.contains("datatables.net-bs5")); assert!(source.contains("datatables.net-bs5"));
assert!(source.contains("paging: true")); assert!(source.contains("paging: true"));
assert!(source.contains("pageLength: 25")); assert!(source.contains("pageLength: 25"));
assert!(source.contains("lengthMenu: [25, 50, 100]")); 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("serverSide: false"));
assert!(source.contains("searching: false"));
assert!(source.contains("ordering: false"));
let html = include_str!("../frontend/main.html"); let html = include_str!("../frontend/main.html");
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] { for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}"); 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("app_handle.exit(0)"));
assert!(tauri.contains("store_runtime_status")); 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"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_security.rs // file: crates/ksp-app-store-desk/tests/desktop_security.rs
// version: 4 // version: 5
//! Security boundary tests for the Store Desk scaffold. //! 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}"); 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\" =>"));
}

View 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
View 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)
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md --> <!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
<!-- version: 7 --> <!-- version: 8 -->
# Plan v0.3.8 — Store Desk V1 RAW # 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. 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 ### 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. Même intégration pour Account, pubkey/range/direction, summaries JS-safe, détail data borné, responsive/scrollX/copy UX.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md --> <!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
<!-- version: 13 --> <!-- version: 14 -->
# Validation v0.3.8 — Store Desk V1 RAW # 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é. - [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é. 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.