v0.3.8-pre.010

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

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 465
# version: 466
[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.9.fix.1"
version = "0.3.8-pre.10"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
@@ -249,6 +249,25 @@
</div>
</div>
<pre id="transactionDetailPayloadPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0"></pre>
<section class="border-top mt-4 pt-4" aria-labelledby="transactionObservationsTitle">
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
<div>
<h3 id="transactionObservationsTitle" class="h6 mb-1">Observations dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source nest chargé.</p>
</div>
</div>
<div id="transactionObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive">
<table id="transactionObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
<thead>
<tr>
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</div>
</div>
</div>
@@ -284,6 +303,25 @@
</div>
</div>
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0"></pre>
<section class="border-top mt-4 pt-4" aria-labelledby="accountObservationsTitle">
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
<div>
<h3 id="accountObservationsTitle" class="h6 mb-1">Observations dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account nest chargé.</p>
</div>
</div>
<div id="accountObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive">
<table id="accountObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
<thead>
<tr>
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th><th>Startup</th><th>Write version</th><th>Tx signature</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
// version: 7
// version: 8
import DataTable from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
@@ -13,6 +13,12 @@ import type { StoreAccountQueryResponseDto } from "./bindings/ksp_app_store_desk
import type { StoreAccountRowDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountRowDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts";
import type { StoreRuntimeStatusDto } from "./bindings/ksp_app_store_desk/dto_common/StoreRuntimeStatusDto.ts";
import type { StoreAccountObservationQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryRequestDto.ts";
import type { StoreAccountObservationQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryResponseDto.ts";
import type { StoreAccountObservationRowDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationRowDto.ts";
import type { StoreTransactionObservationQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryRequestDto.ts";
import type { StoreTransactionObservationQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryResponseDto.ts";
import type { StoreTransactionObservationRowDto } from "./bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationRowDto.ts";
import type { StoreTransactionDetailDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts";
import type { StoreTransactionDetailRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailRequestDto.ts";
import type { StoreTransactionQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryRequestDto.ts";
@@ -54,6 +60,10 @@ const viewTitles: Record<ViewId, string> = {
let transactionTable: StoreDataTable | null = null;
let accountTable: StoreDataTable | null = null;
let transactionObservationTable: StoreDataTable | null = null;
let accountObservationTable: StoreDataTable | null = null;
let currentTransactionObservationSignature: string | null = null;
let currentAccountObservationIdentity: { pubkey: string; slot: string; stateHash: string } | null = null;
const COPY_FEEDBACK_MILLISECONDS = 1200;
const LONG_TEXT_HEAD_CHARACTERS = 12;
@@ -211,6 +221,32 @@ function decimalCountToSafeNumber(value: string): number | null {
return Number(exact);
}
function optionalLongText(value: string | null, fieldId: string): string {
return value === null ? "—" : renderCopyableLongText(value, fieldId);
}
function observationPageRequest(page: DataTablesPageRequest): { draw: number; limit: number; offset: string } | null {
if (!Number.isSafeInteger(page.draw) || page.draw < 0 || !Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) {
return null;
}
return { draw: page.draw, limit: page.length, offset: page.start.toString() };
}
function renderObservationCode(value: string | null, fieldId: string): string {
return optionalLongText(value, fieldId);
}
function renderOrigin(origin: string): string {
return `<span class="badge text-bg-secondary">${escapeHtml(origin)}</span>`;
}
function renderOptionalBoolean(value: boolean | null): string {
if (value === null) {
return "—";
}
return value ? '<span class="badge text-bg-info">oui</span>' : '<span class="badge text-bg-secondary">non</span>';
}
function currentTransactionQuery(page: DataTablesPageRequest): StoreTransactionQueryRequestDto | null {
if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) {
return null;
@@ -461,6 +497,176 @@ function initializeAccountTable(): void {
frontendTrace("main", "Store Desk account DataTable initialized", { pagingOwner: "datatables", serverSide: true });
}
function emptyTransactionObservationPage(draw: number): DataTablesPageResponse<StoreTransactionObservationRowDto> {
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
}
function emptyAccountObservationPage(draw: number): DataTablesPageResponse<StoreAccountObservationRowDto> {
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
}
function setTransactionObservationError(message: string | null): void {
const element = document.querySelector<HTMLElement>("#transactionObservationsError");
if (!element) {
return;
}
element.hidden = message === null;
element.textContent = message ?? "";
}
function setAccountObservationError(message: string | null): void {
const element = document.querySelector<HTMLElement>("#accountObservationsError");
if (!element) {
return;
}
element.hidden = message === null;
element.textContent = message ?? "";
}
async function queryTransactionObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreTransactionObservationRowDto>) => void): Promise<void> {
const pagination = observationPageRequest(page);
const signature = currentTransactionObservationSignature;
if (signature === null) {
callback(emptyTransactionObservationPage(pagination?.draw ?? 0));
return;
}
if (pagination === null) {
setTransactionObservationError("La pagination des observations n'est pas admise par Store Desk.");
callback(emptyTransactionObservationPage(0));
frontendError("main", "Store Desk transaction observation DataTables query rejected before IPC");
return;
}
const request: StoreTransactionObservationQueryRequestDto = { limit: pagination.limit, offset: pagination.offset, signature };
setTransactionObservationError(null);
frontendDebug("main", "Store Desk transaction observation DataTables server-side query started", { pageLength: request.limit });
try {
const response = await invokeKsp<StoreTransactionObservationQueryResponseDto>("main", "store_query_transaction_observations", { request });
if (currentTransactionObservationSignature !== signature) {
callback(emptyTransactionObservationPage(pagination.draw));
frontendTrace("main", "Store Desk stale transaction observation response ignored");
return;
}
const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal);
const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal);
if (recordsTotal === null || recordsFiltered === null) {
setTransactionObservationError("Le volume d'observations dépasse le domaine entier sûr de l'interface.");
callback(emptyTransactionObservationPage(pagination.draw));
frontendError("main", "Store Desk transaction observation exact count exceeds frontend safe integer domain");
return;
}
callback({ data: response.rows, draw: pagination.draw, recordsFiltered, recordsTotal });
frontendDebug("main", "Store Desk transaction observation DataTables server-side query completed", { rowCount: response.rows.length });
} catch {
setTransactionObservationError("La requête d'observations Transaction a échoué.");
callback(emptyTransactionObservationPage(pagination.draw));
frontendError("main", "Store Desk transaction observation DataTables server-side query failed");
}
}
async function queryAccountObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreAccountObservationRowDto>) => void): Promise<void> {
const pagination = observationPageRequest(page);
const identity = currentAccountObservationIdentity;
if (identity === null) {
callback(emptyAccountObservationPage(pagination?.draw ?? 0));
return;
}
if (pagination === null) {
setAccountObservationError("La pagination des observations n'est pas admise par Store Desk.");
callback(emptyAccountObservationPage(0));
frontendError("main", "Store Desk account observation DataTables query rejected before IPC");
return;
}
const request: StoreAccountObservationQueryRequestDto = {
limit: pagination.limit,
offset: pagination.offset,
pubkey: identity.pubkey,
slot: identity.slot,
stateHash: identity.stateHash,
};
setAccountObservationError(null);
frontendDebug("main", "Store Desk account observation DataTables server-side query started", { pageLength: request.limit });
try {
const response = await invokeKsp<StoreAccountObservationQueryResponseDto>("main", "store_query_account_observations", { request });
if (currentAccountObservationIdentity !== identity) {
callback(emptyAccountObservationPage(pagination.draw));
frontendTrace("main", "Store Desk stale account observation response ignored");
return;
}
const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal);
const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal);
if (recordsTotal === null || recordsFiltered === null) {
setAccountObservationError("Le volume d'observations dépasse le domaine entier sûr de l'interface.");
callback(emptyAccountObservationPage(pagination.draw));
frontendError("main", "Store Desk account observation exact count exceeds frontend safe integer domain");
return;
}
callback({ data: response.rows, draw: pagination.draw, recordsFiltered, recordsTotal });
frontendDebug("main", "Store Desk account observation DataTables server-side query completed", { rowCount: response.rows.length });
} catch {
setAccountObservationError("La requête d'observations Account a échoué.");
callback(emptyAccountObservationPage(pagination.draw));
frontendError("main", "Store Desk account observation DataTables server-side query failed");
}
}
function provenanceColumns(prefix: string) {
return [
{ className: "font-monospace", data: "provenance.receivedAtUnixMillisDecimal" },
{ className: "font-monospace", data: "provenance.observedAtUnixMillisDecimal", defaultContent: "—" },
{ data: "provenance.provider", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-provider`) : String(data)) },
{ data: "provenance.protocol", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-protocol`) : String(data)) },
{ data: "provenance.acquisitionMethod", render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(String(data), `${prefix}-method`) : String(data)) },
{ data: "provenance.origin", render: (data: unknown, type: string) => (type === "display" ? renderOrigin(String(data)) : String(data)) },
{ data: "provenance.endpointId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-endpoint`) : data) },
{ data: "provenance.commitment", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-commitment`) : data) },
{ data: "provenance.captureSessionId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-session`) : data) },
{ data: "provenance.filterId", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-filter`) : data) },
{ data: "provenance.sourcePayloadHash", defaultContent: null, render: (data: unknown, type: string) => (type === "display" ? renderObservationCode(data === null ? null : String(data), `${prefix}-source-hash`) : data) },
{ className: "font-monospace", data: "provenance.sourcePayloadSizeDecimal", defaultContent: "—" },
];
}
function initializeTransactionObservationTable(): void {
transactionObservationTable = new DataTable("#transactionObservationsTable", {
ajax: (data, callback) => {
void queryTransactionObservations(data as unknown as DataTablesPageRequest, response => callback(response));
},
autoWidth: false,
columns: [
...provenanceColumns("transaction-observation"),
{ data: "observationKey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-observation-key") : String(data)) },
],
info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true,
language: { emptyTable: "Aucune observation pour cette transaction.", info: "_START_ à _END_ sur _TOTAL_ observation(s)", infoEmpty: "0 observation", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucune observation correspondante." },
}) as unknown as StoreDataTable;
transactionObservationTable.on("page", () => frontendDebug("main", "Store Desk transaction observation DataTables page changed"));
transactionObservationTable.on("length", () => frontendDebug("main", "Store Desk transaction observation DataTables page length changed"));
transactionObservationTable.on("draw", () => frontendTrace("main", "Store Desk transaction observation DataTables redraw completed"));
frontendTrace("main", "Store Desk transaction observation DataTable initialized", { pagingOwner: "datatables", serverSide: true });
}
function initializeAccountObservationTable(): void {
accountObservationTable = new DataTable("#accountObservationsTable", {
ajax: (data, callback) => {
void queryAccountObservations(data as unknown as DataTablesPageRequest, response => callback(response));
},
autoWidth: false,
columns: [
...provenanceColumns("account-observation"),
{ data: "observationKey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-observation-key") : String(data)) },
{ data: "isStartup", defaultContent: null, render: (data, type) => (type === "display" ? renderOptionalBoolean(data === null ? null : Boolean(data)) : data) },
{ className: "font-monospace", data: "writeVersionDecimal", defaultContent: "—" },
{ data: "transactionSignature", defaultContent: null, render: (data, type) => (type === "display" ? optionalLongText(data === null ? null : String(data), "account-observation-transaction-signature") : data) },
],
info: true, lengthMenu: [25, 50, 100], ordering: false, pageLength: 25, paging: true, processing: true, searching: false, serverSide: true, scrollX: true,
language: { emptyTable: "Aucune observation pour cet état Account.", info: "_START_ à _END_ sur _TOTAL_ observation(s)", infoEmpty: "0 observation", lengthMenu: "Afficher _MENU_", loadingRecords: "Chargement Store…", processing: "Inspection Store…", zeroRecords: "Aucune observation correspondante." },
}) as unknown as StoreDataTable;
accountObservationTable.on("page", () => frontendDebug("main", "Store Desk account observation DataTables page changed"));
accountObservationTable.on("length", () => frontendDebug("main", "Store Desk account observation DataTables page length changed"));
accountObservationTable.on("draw", () => frontendTrace("main", "Store Desk account observation DataTables redraw completed"));
frontendTrace("main", "Store Desk account observation DataTable initialized", { pagingOwner: "datatables", serverSide: true });
}
function renderShellStatus(status: ShellStatusDto): void {
setText("appVersionBadge", status.applicationVersion);
setText("runtimeVersion", status.applicationVersion);
@@ -525,6 +731,9 @@ async function refreshDiagnostics(): Promise<void> {
}
function clearAccountDetail(): void {
currentAccountObservationIdentity = null;
setAccountObservationError(null);
accountObservationTable?.draw(true);
for (const elementId of [
"accountDetailPubkey",
"accountDetailSlot",
@@ -588,6 +797,8 @@ async function openAccountDetail(pubkey: string, slot: string, stateHash: string
try {
const detail = await invokeKsp<StoreAccountDetailDto>("main", "store_get_account_detail", { request });
renderAccountDetail(detail);
currentAccountObservationIdentity = { pubkey, slot, stateHash };
accountObservationTable?.draw(true);
frontendDebug("main", "Store Desk account detail opened", { dataPreviewTruncated: detail.dataPreviewTruncated });
} catch {
setText("accountDetailError", "Le détail de cet état Account n'a pas pu être chargé.");
@@ -602,6 +813,9 @@ function redrawAccounts(resetPaging: boolean, reason: string): void {
}
function clearTransactionDetail(): void {
currentTransactionObservationSignature = null;
setTransactionObservationError(null);
transactionObservationTable?.draw(true);
for (const elementId of [
"transactionDetailSignature",
"transactionDetailSlot",
@@ -661,6 +875,8 @@ async function openTransactionDetail(signature: string): Promise<void> {
try {
const detail = await invokeKsp<StoreTransactionDetailDto>("main", "store_get_transaction_detail", { request });
renderTransactionDetail(detail);
currentTransactionObservationSignature = signature;
transactionObservationTable?.draw(true);
frontendDebug("main", "Store Desk transaction detail opened", { payloadPreviewTruncated: detail.payloadPreviewTruncated, retentionState: detail.retentionState });
} catch {
setText("transactionDetailError", "Le détail de cette transaction n'a pas pu être chargé.");
@@ -780,11 +996,17 @@ function installInteractions(): void {
});
});
const accountDetailModal = document.querySelector<HTMLElement>("#accountDetailModal");
accountDetailModal?.addEventListener("shown.bs.modal", () => {
accountObservationTable?.columns.adjust();
});
accountDetailModal?.addEventListener("hidden.bs.modal", () => {
clearAccountDetail();
frontendDebug("main", "Store Desk account detail closed");
});
const detailModal = document.querySelector<HTMLElement>("#transactionDetailModal");
detailModal?.addEventListener("shown.bs.modal", () => {
transactionObservationTable?.columns.adjust();
});
detailModal?.addEventListener("hidden.bs.modal", () => {
clearTransactionDetail();
frontendDebug("main", "Store Desk transaction detail closed");
@@ -796,6 +1018,8 @@ async function initializeMain(): Promise<void> {
frontendInfo("main", "Store Desk main frontend loaded");
initializeTransactionTable();
initializeAccountTable();
initializeTransactionObservationTable();
initializeAccountObservationTable();
installInteractions();
activateView("overview");
await refreshDiagnostics();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/app_state.rs
// version: 4
// version: 5
//! 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.008-raw-accounts".to_owned(),
shell_phase: "pre.010-observations-ui".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
@@ -125,6 +125,14 @@ impl crate::AppState {
return self.store_startup.account_detail(request).await;
}
/// Executes one bounded metadata-only observation inspection for one explicit RAW account state.
pub(crate) async fn query_account_observations(
&self,
request: crate::StoreAccountObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
return self.store_startup.query_account_observations(request).await;
}
/// Executes one bounded RAW transaction inspection query through the retained Store facade.
pub(crate) async fn query_transactions(
&self,
@@ -138,6 +146,14 @@ impl crate::AppState {
return self.store_startup.transaction_detail(request).await;
}
/// Executes one bounded metadata-only observation inspection for one explicit RAW transaction.
pub(crate) async fn query_transaction_observations(
&self,
request: crate::StoreTransactionObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
return self.store_startup.query_transaction_observations(request).await;
}
/// Returns the resolved splash timings captured during application bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {

View File

@@ -0,0 +1,125 @@
// file: crates/ksp-app-store-desk/src/dto_observation.rs
// version: 1
//! Application-owned DTOs for safe RAW observation inspection inside entity detail modals.
use ts_rs::TS; // rust-rules: trait-import
/// Safe source-independent acquisition provenance projected for Store Desk observation tables.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreObservationProvenanceDto.ts")]
pub(crate) struct StoreObservationProvenanceDto {
/// Logical acquisition-method code.
pub(crate) acquisition_method: String,
/// Optional bounded capture/session identifier.
pub(crate) capture_session_id: std::option::Option<String>,
/// Optional commitment code captured at acquisition.
pub(crate) commitment: std::option::Option<String>,
/// Optional Config-owned logical endpoint identifier.
pub(crate) endpoint_id: std::option::Option<String>,
/// Optional bounded logical filter identifier.
pub(crate) filter_id: std::option::Option<String>,
/// Optional source-observation timestamp encoded as exact Unix-millisecond decimal text.
pub(crate) observed_at_unix_millis_decimal: std::option::Option<String>,
/// Stable acquisition-origin code.
pub(crate) origin: String,
/// Logical transport/protocol code.
pub(crate) protocol: String,
/// Safe provider code.
pub(crate) provider: String,
/// Local receipt timestamp encoded as exact Unix-millisecond decimal text.
pub(crate) received_at_unix_millis_decimal: String,
/// Optional source-wire payload digest encoded as lower-case hexadecimal text, never source bytes.
pub(crate) source_payload_hash: std::option::Option<String>,
/// Optional source-wire payload size encoded as exact decimal text.
pub(crate) source_payload_size_decimal: std::option::Option<String>,
}
/// DataTables random-access request for observations of one explicit RAW transaction.
#[derive(Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryRequestDto.ts")]
pub(crate) struct StoreTransactionObservationQueryRequestDto {
/// Whitelisted page size (`25`, `50`, or `100`).
pub(crate) limit: u32,
/// Absolute zero-based observation offset encoded as exact decimal text.
pub(crate) offset: String,
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
pub(crate) signature: String,
}
/// Safe metadata-only row for one RAW transaction observation.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationRowDto.ts")]
pub(crate) struct StoreTransactionObservationRowDto {
/// Deterministic observation key encoded as lower-case hexadecimal text.
pub(crate) observation_key: String,
/// Safe source-independent acquisition provenance.
pub(crate) provenance: crate::StoreObservationProvenanceDto,
}
/// Server-side transaction-observation 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_observation/StoreTransactionObservationQueryResponseDto.ts")]
pub(crate) struct StoreTransactionObservationQueryResponseDto {
/// Exact count after the mandatory transaction filter.
pub(crate) records_filtered_decimal: String,
/// Exact count in the mandatory Store network scope before the entity filter.
pub(crate) records_total_decimal: String,
/// Metadata-only observation rows for the requested random-access page.
pub(crate) rows: std::vec::Vec<crate::StoreTransactionObservationRowDto>,
}
/// DataTables random-access request for observations of one explicit RAW account state.
#[derive(Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryRequestDto.ts")]
pub(crate) struct StoreAccountObservationQueryRequestDto {
/// Whitelisted page size (`25`, `50`, or `100`).
pub(crate) limit: u32,
/// Absolute zero-based observation offset encoded as exact decimal text.
pub(crate) offset: String,
/// Account public key in canonical base58 text.
pub(crate) pubkey: String,
/// Solana slot encoded as exact decimal text.
pub(crate) slot: String,
/// Canonical account-state hash encoded as lower-case hexadecimal text.
pub(crate) state_hash: String,
}
/// Safe metadata-only row for one RAW account observation.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationRowDto.ts")]
pub(crate) struct StoreAccountObservationRowDto {
/// Optional source-reported startup/replay marker.
pub(crate) is_startup: std::option::Option<bool>,
/// Deterministic observation key encoded as lower-case hexadecimal text.
pub(crate) observation_key: String,
/// Safe source-independent acquisition provenance.
pub(crate) provenance: crate::StoreObservationProvenanceDto,
/// Optional transaction signature associated with the account write, encoded as lower-case hexadecimal text.
pub(crate) transaction_signature: std::option::Option<String>,
/// Optional source-specific account write version encoded as exact decimal text.
pub(crate) write_version_decimal: std::option::Option<String>,
}
/// Server-side account-observation 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_observation/StoreAccountObservationQueryResponseDto.ts")]
pub(crate) struct StoreAccountObservationQueryResponseDto {
/// Exact count after the mandatory account-state filter.
pub(crate) records_filtered_decimal: String,
/// Exact count in the mandatory Store network scope before the entity filter.
pub(crate) records_total_decimal: String,
/// Metadata-only observation rows for the requested random-access page.
pub(crate) rows: std::vec::Vec<crate::StoreAccountObservationRowDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_observation.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/lib.rs
// version: 5
// version: 6
//! Tauri desktop scaffold for backend-neutral KSP Store inspection.
@@ -12,6 +12,7 @@ mod bootstrap;
mod constants;
mod dto_account;
mod dto_common;
mod dto_observation;
mod dto_transaction;
mod errors;
mod frontend_logging;
@@ -77,6 +78,20 @@ 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;
/// DataTables random-access request for observations of one explicit RAW account state.
pub(crate) use self::dto_observation::StoreAccountObservationQueryRequestDto;
/// Server-side account-observation table result using exact decimal counts.
pub(crate) use self::dto_observation::StoreAccountObservationQueryResponseDto;
/// Safe metadata-only row for one RAW account observation.
pub(crate) use self::dto_observation::StoreAccountObservationRowDto;
/// Safe source-independent acquisition provenance projected for observation tables.
pub(crate) use self::dto_observation::StoreObservationProvenanceDto;
/// DataTables random-access request for observations of one explicit RAW transaction.
pub(crate) use self::dto_observation::StoreTransactionObservationQueryRequestDto;
/// Server-side transaction-observation table result using exact decimal counts.
pub(crate) use self::dto_observation::StoreTransactionObservationQueryResponseDto;
/// Safe metadata-only row for one RAW transaction observation.
pub(crate) use self::dto_observation::StoreTransactionObservationRowDto;
/// 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.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/store_runtime.rs
// version: 4
// version: 5
//! Composite-selected Store readiness, status and bounded shutdown lifecycle owned by Store Desk.
@@ -53,6 +53,18 @@ impl crate::StoreStartup {
};
}
/// Executes one bounded metadata-only observation inspection for an explicit RAW account state.
pub(crate) async fn query_account_observations(
&self,
request: crate::StoreAccountObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
let runtime = self.runtime.as_ref();
return match runtime {
std::option::Option::Some(value) => value.query_account_observations(request).await,
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
};
}
/// Executes one bounded server-side RAW transaction inspection query.
pub(crate) async fn query_transactions(
&self,
@@ -74,6 +86,18 @@ impl crate::StoreStartup {
};
}
/// Executes one bounded metadata-only observation inspection for an explicit RAW transaction.
pub(crate) async fn query_transaction_observations(
&self,
request: crate::StoreTransactionObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
let runtime = self.runtime.as_ref();
return match runtime {
std::option::Option::Some(value) => value.query_transaction_observations(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();
@@ -184,6 +208,37 @@ impl crate::StoreRuntime {
return std::result::Result::Ok(account_detail_dto(&state));
}
/// Executes one validated metadata-only observation query for one exact account-state identity.
pub(crate) async fn query_account_observations(
&self,
request: crate::StoreAccountObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
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 = account_observation_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::RawAccountObservationInspectionRead::inspect_raw_account_observations(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(account_observation_row_dto(&item));
}
return std::result::Result::Ok(crate::StoreAccountObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
}
/// Executes one validated random-access transaction inspection query while retaining the Store read guard.
pub(crate) async fn query_transactions(
&self,
@@ -246,6 +301,37 @@ impl crate::StoreRuntime {
};
}
/// Executes one validated metadata-only observation query for one exact transaction identity.
pub(crate) async fn query_transaction_observations(
&self,
request: crate::StoreTransactionObservationQueryRequestDto,
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
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_observation_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::RawTransactionObservationInspectionRead::inspect_raw_transaction_observations(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_observation_row_dto(&item));
}
return std::result::Result::Ok(crate::StoreTransactionObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
}
/// 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;
@@ -281,23 +367,88 @@ impl crate::StoreRuntime {
const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;
const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;
fn account_query_from_request(
store: &ksp_store_lib::Store,
request: &crate::StoreAccountQueryRequestDto,
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateInspectionQuery> {
let limit = match request.limit {
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(request.limit)),
fn inspection_page_from_request(offset_text: &str, limit_value: u32) -> ksp_core_lib::Result<ksp_store_lib::RawInspectionPageRequest> {
let limit = match limit_value {
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(limit_value)),
_ => 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 = offset_text.parse::<u64>();
let offset = match offset {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
};
return std::result::Result::Ok(ksp_store_lib::RawInspectionPageRequest::new(offset, limit));
}
fn account_observation_query_from_request(
store: &ksp_store_lib::Store,
request: &crate::StoreAccountObservationQueryRequestDto,
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountObservationInspectionQuery> {
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
let page = match page {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let detail_request =
crate::StoreAccountDetailRequestDto { pubkey: request.pubkey.clone(), slot: request.slot.clone(), state_hash: request.state_hash.clone() };
let reference = account_reference_from_request(store, &detail_request);
let reference = match reference {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let network = store.runtime_snapshot().network().clone();
let query = ksp_store_lib::RawAccountObservationInspectionQuery::try_new(
network,
std::option::Option::Some(reference),
ksp_store_lib::RawSortDirection::Descending,
page,
);
return match query {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
};
}
fn transaction_observation_query_from_request(
store: &ksp_store_lib::Store,
request: &crate::StoreTransactionObservationQueryRequestDto,
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionObservationInspectionQuery> {
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
let page = match page {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 network = store.runtime_snapshot().network().clone();
let query = ksp_store_lib::RawTransactionObservationInspectionQuery::try_new(
network,
std::option::Option::Some(reference),
ksp_store_lib::RawSortDirection::Descending,
page,
);
return match query {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
};
}
fn account_query_from_request(
store: &ksp_store_lib::Store,
request: &crate::StoreAccountQueryRequestDto,
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateInspectionQuery> {
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
let page = match page {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pubkey = parse_optional_pubkey(request.pubkey.as_deref());
let pubkey = match pubkey {
std::result::Result::Ok(value) => value,
@@ -324,7 +475,6 @@ fn account_query_from_request(
_ => 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::RawAccountStateInspectionQuery::new(network, pubkey, slots, direction, page));
}
@@ -383,22 +533,59 @@ fn account_row_dto(summary: &ksp_store_lib::RawAccountStateSummary) -> crate::St
};
}
fn observation_provenance_dto(provenance: &ksp_store_lib::RawAcquisitionProvenance) -> crate::StoreObservationProvenanceDto {
return crate::StoreObservationProvenanceDto {
acquisition_method: provenance.acquisition_method().as_str().to_owned(),
capture_session_id: provenance.capture_session_id().map(|value| return value.as_str().to_owned()),
commitment: provenance.commitment().map(|value| return value.as_str().to_owned()),
endpoint_id: provenance.endpoint_id().map(|value| return value.as_str().to_owned()),
filter_id: provenance.filter_id().map(|value| return value.as_str().to_owned()),
observed_at_unix_millis_decimal: provenance.observed_at().map(|value| return value.unix_millis().to_string()),
origin: acquisition_origin_code(provenance.origin()).to_owned(),
protocol: provenance.protocol().as_str().to_owned(),
provider: provenance.provider().as_str().to_owned(),
received_at_unix_millis_decimal: provenance.received_at().unix_millis().to_string(),
source_payload_hash: provenance.source_payload_hash().map(|value| return encode_hex(value.as_bytes())),
source_payload_size_decimal: provenance.source_payload_size_bytes().map(|value| return value.to_string()),
};
}
fn account_observation_row_dto(summary: &ksp_store_lib::RawAccountObservationSummary) -> crate::StoreAccountObservationRowDto {
return crate::StoreAccountObservationRowDto {
is_startup: summary.is_startup(),
observation_key: encode_hex(summary.observation_key().as_bytes()),
provenance: observation_provenance_dto(summary.provenance()),
transaction_signature: summary.transaction_signature().map(|value| return encode_hex(value.as_bytes())),
write_version_decimal: summary.write_version().map(|value| return value.to_string()),
};
}
fn transaction_observation_row_dto(summary: &ksp_store_lib::RawTransactionObservationSummary) -> crate::StoreTransactionObservationRowDto {
return crate::StoreTransactionObservationRowDto {
observation_key: encode_hex(summary.observation_key().as_bytes()),
provenance: observation_provenance_dto(summary.provenance()),
};
}
fn acquisition_origin_code(origin: ksp_store_lib::RawAcquisitionOrigin) -> &'static str {
return match origin {
ksp_store_lib::RawAcquisitionOrigin::Backfill => "backfill",
ksp_store_lib::RawAcquisitionOrigin::Import => "import",
ksp_store_lib::RawAcquisitionOrigin::Live => "live",
ksp_store_lib::RawAcquisitionOrigin::Repair => "repair",
ksp_store_lib::RawAcquisitionOrigin::Replay => "replay",
_ => "unknown",
};
}
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 {
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
let page = match page {
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()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
let slot_min = match slot_min {
@@ -421,7 +608,6 @@ fn transaction_query_from_request(
_ => 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));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/src/tauri.rs
// version: 4
// version: 5
//! Tauri runtime assembly for the KSP Store desktop application.
@@ -81,7 +81,9 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
splash_frontend_ready,
store_get_account_detail,
store_get_transaction_detail,
store_query_account_observations,
store_query_accounts,
store_query_transaction_observations,
store_query_transactions,
store_runtime_status,
]);
@@ -176,6 +178,20 @@ fn get_shell_status(state: tauri::State<'_, crate::AppState>) -> std::result::Re
};
}
#[tauri::command]
async fn store_query_account_observations(
request: crate::StoreAccountObservationQueryRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::StoreAccountObservationQueryResponseDto, crate::CommandErrorDto> {
let result = state.query_account_observations(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_account_observations", crate::TRACING_DOMAIN_STORE, &error))
},
};
}
#[tauri::command]
async fn store_query_accounts(
request: crate::StoreAccountQueryRequestDto,
@@ -200,6 +216,20 @@ async fn store_get_account_detail(
};
}
#[tauri::command]
async fn store_query_transaction_observations(
request: crate::StoreTransactionObservationQueryRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::StoreTransactionObservationQueryResponseDto, crate::CommandErrorDto> {
let result = state.query_transaction_observations(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_transaction_observations", crate::TRACING_DOMAIN_STORE, &error))
},
};
}
#[tauri::command]
async fn store_query_transactions(
request: crate::StoreTransactionQueryRequestDto,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
// version: 6
// version: 7
//! Dependency-boundary tests for the Store Desk application.
@@ -131,3 +131,34 @@ fn pre_008_account_inspection_uses_store_facade_and_new_visible_items_use_crate_
assert!(!sources.contains(forbidden), "Store Desk account inspection crossed physical/backend-neutral boundary: {forbidden}");
}
}
#[test]
fn pre_010_observation_ui_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
let lib = include_str!("../src/lib.rs");
let dto = include_str!("../src/dto_observation.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_observation::StoreAccountObservationQueryRequestDto;",
"pub(crate) use self::dto_observation::StoreAccountObservationQueryResponseDto;",
"pub(crate) use self::dto_observation::StoreAccountObservationRowDto;",
"pub(crate) use self::dto_observation::StoreObservationProvenanceDto;",
"pub(crate) use self::dto_observation::StoreTransactionObservationQueryRequestDto;",
"pub(crate) use self::dto_observation::StoreTransactionObservationQueryResponseDto;",
"pub(crate) use self::dto_observation::StoreTransactionObservationRowDto;",
] {
assert!(lib.contains(expected), "missing pre.010 crate-root observation DTO re-export: {expected}");
}
assert!(dto.contains("crate::StoreObservationProvenanceDto"));
assert!(runtime.contains("ksp_store_lib::RawTransactionObservationInspectionRead::inspect_raw_transaction_observations"));
assert!(runtime.contains("ksp_store_lib::RawAccountObservationInspectionRead::inspect_raw_account_observations"));
assert!(app_state.contains("request: crate::StoreTransactionObservationQueryRequestDto"));
assert!(app_state.contains("request: crate::StoreAccountObservationQueryRequestDto"));
assert!(tauri.contains("request: crate::StoreTransactionObservationQueryRequestDto"));
assert!(tauri.contains("request: crate::StoreAccountObservationQueryRequestDto"));
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 observation UI crossed physical/backend-neutral boundary: {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
// version: 8
// version: 9
//! Desktop template and frontend contract tests for the Store Desk scaffold.
@@ -272,3 +272,58 @@ fn pre_008_account_datatable_is_server_side_copyable_and_uses_datatables_as_only
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
}
}
#[test]
fn pre_010_observations_are_server_side_inside_entity_details_with_datatables_as_only_pager() {
let html = include_str!("../frontend/main.html");
let frontend = include_str!("../frontend/ts/main.ts");
let tauri = include_str!("../src/tauri.rs");
let runtime = include_str!("../src/store_runtime.rs");
for expected in [
"id=\"transactionObservationsTable\"",
"id=\"accountObservationsTable\"",
"id=\"transactionObservationsError\"",
"id=\"accountObservationsError\"",
] {
assert!(html.contains(expected), "missing pre.010 observation UI contract: {expected}");
}
assert!(frontend.contains("store_query_transaction_observations"));
assert!(frontend.contains("store_query_account_observations"));
assert!(frontend.contains("initializeTransactionObservationTable"));
assert!(frontend.contains("initializeAccountObservationTable"));
assert!(frontend.contains("currentTransactionObservationSignature"));
assert!(frontend.contains("currentAccountObservationIdentity"));
assert!(frontend.contains("stale transaction observation response ignored"));
assert!(frontend.contains("stale account observation response ignored"));
assert!(tauri.contains("store_query_transaction_observations"));
assert!(tauri.contains("store_query_account_observations"));
assert!(runtime.contains("RawSortDirection::Descending"));
assert!(!html.contains("store-observation-pager"));
assert!(!frontend.contains("cursor:"));
}
#[test]
fn pre_010_observation_rows_project_safe_provenance_and_reuse_long_text_copy_without_source_bytes() {
let dto = include_str!("../src/dto_observation.rs");
let frontend = include_str!("../frontend/ts/main.ts");
for expected in [
"source_payload_hash",
"source_payload_size_decimal",
"received_at_unix_millis_decimal",
"observed_at_unix_millis_decimal",
"capture_session_id",
"endpoint_id",
"filter_id",
"transaction_signature",
"write_version_decimal",
] {
assert!(dto.contains(expected), "missing safe observation metadata field: {expected}");
}
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
assert!(!dto.contains(forbidden), "observation DTO leaked forbidden bytes field: {forbidden}");
}
assert!(frontend.contains("transaction-observation-key"));
assert!(frontend.contains("account-observation-key"));
assert!(frontend.contains("account-observation-transaction-signature"));
assert!(frontend.contains("renderCopyableLongText"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
// version: 9
// version: 10
//! Security boundary tests for the Store Desk scaffold.
@@ -230,3 +230,37 @@ fn pre_008_account_copy_tracing_never_logs_identity_or_data_material() {
assert!(!frontend.contains(forbidden), "account copy tracing leaked copied value material: {forbidden}");
}
}
#[test]
fn pre_010_observation_ipc_and_tracing_keep_entity_values_and_source_bytes_out_of_logs() {
let dto = include_str!("../src/dto_observation.rs");
let frontend = include_str!("../frontend/ts/main.ts");
let tauri = include_str!("../src/tauri.rs");
for forbidden in ["draw:", "search:", "order:", "columns:", "cursor:", "network:"] {
assert!(!dto.contains(forbidden), "observation IPC request leaked DataTables/physical field: {forbidden}");
}
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
assert!(!dto.contains(forbidden), "observation IPC leaked raw/source bytes: {forbidden}");
}
assert!(frontend.contains(r#"Store Desk transaction observation DataTables server-side query started", { pageLength: request.limit }"#));
assert!(frontend.contains(r#"Store Desk account observation DataTables server-side query started", { pageLength: request.limit }"#));
assert!(!frontend.contains(r#"query started", { signature"#));
assert!(!frontend.contains(r#"query started", { pubkey"#));
assert!(!frontend.contains(r#"query completed", { observationKey"#));
assert!(tauri.contains("project_command_error("store_query_transaction_observations""));
assert!(tauri.contains("project_command_error("store_query_account_observations""));
}
#[test]
fn pre_010_observation_page_hardening_rejects_hostile_draw_length_and_unsafe_exact_counts() {
let frontend = include_str!("../frontend/ts/main.ts");
let runtime = include_str!("../src/store_runtime.rs");
assert!(frontend.contains("Number.isSafeInteger(page.draw)"));
assert!(frontend.contains("Number.isSafeInteger(page.start)"));
assert!(frontend.contains("[25, 50, 100].includes(page.length)"));
assert!(frontend.contains("Number.MAX_SAFE_INTEGER"));
assert!(frontend.contains("exact count exceeds frontend safe integer domain"));
assert!(runtime.contains("inspection_page_from_request"));
assert!(runtime.contains("request.offset.as_str(), request.limit"));
assert!(runtime.contains("offset_text.parse::<u64>()"));
}

View File

@@ -0,0 +1,47 @@
// file: crates/ksp-app-store-desk/unit_tests/dto_observation.rs
// version: 1
//! Unit tests for Store Desk observation DTO safety and request boundaries.
#[test]
fn pre_010_observation_requests_expose_only_random_access_and_exact_entity_identity() {
let source = include_str!("../src/dto_observation.rs");
for forbidden in ["draw:", "search:", "order:", "columns:", "cursor:", "network:", "payload_preview", "data_preview"] {
assert!(!source.contains(forbidden), "observation request/row contract leaked forbidden field: {forbidden}");
}
}
#[test]
fn pre_010_observation_rows_keep_source_bytes_out_and_preserve_safe_provenance_metadata() {
let provenance = crate::StoreObservationProvenanceDto {
acquisition_method: "subscribe".to_owned(),
capture_session_id: std::option::Option::Some("session-1".to_owned()),
commitment: std::option::Option::Some("confirmed".to_owned()),
endpoint_id: std::option::Option::Some("mainnet-primary".to_owned()),
filter_id: std::option::Option::Some("wallet-filter".to_owned()),
observed_at_unix_millis_decimal: std::option::Option::Some("10".to_owned()),
origin: "live".to_owned(),
protocol: "yellowstone-grpc".to_owned(),
provider: "publicnode".to_owned(),
received_at_unix_millis_decimal: "11".to_owned(),
source_payload_hash: std::option::Option::Some("12".repeat(32)),
source_payload_size_decimal: std::option::Option::Some("2048".to_owned()),
};
let row = crate::StoreAccountObservationRowDto {
is_startup: std::option::Option::Some(false),
observation_key: "34".repeat(32),
provenance,
transaction_signature: std::option::Option::Some("56".repeat(64)),
write_version_decimal: std::option::Option::Some("9".to_owned()),
};
let json = serde_json::to_string(&row);
let json = match json {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("observation DTO serialization failed: {error}"),
};
assert!(json.contains("sourcePayloadHash"));
assert!(json.contains("sourcePayloadSizeDecimal"));
for forbidden in ["payloadPreview", "dataPreview", "rawBytes", "sourcePayloadBytes"] {
assert!(!json.contains(forbidden), "observation DTO serialized forbidden source material: {forbidden}");
}
}

52
deltas/0.3.8/pre.010.md Normal file
View File

@@ -0,0 +1,52 @@
<!-- file: deltas/0.3.8/pre.010.md -->
<!-- version: 1 -->
# Delta `0.3.8-pre.010`
## Base
- livraison directe : `0.3.8-pre.009-fix.001` ;
- workspace : `0.3.8-pre.9.fix.1` ;
- gate opérateur précédent : entièrement vert, sans warning Clippy.
## Objet
Intégrer l'inspection des observations RAW dans les détails Store Desk et durcir le contrat DataTables/IPC V1, sans modification Store API/PostgreSQL/schema.
## Changements
- ajout de `dto_observation.rs` et de ses tests ;
- deux queries IPC metadata-only : Transaction observations et Account observations ;
- deux DataTables server-side dans les modals existants ;
- provenance sûre complète sans source payload bytes ;
- metadata Account optionnelles (`is_startup`, `write_version`, transaction signature) ;
- troncature/tooltip/copie réutilisée pour les chaînes longues ;
- validation commune Rust des offset/limit, whitelist 25/50/100 ;
- hardening draw/start/length, counts JS sûrs et réponses stale ;
- règle `pub`/`pub(crate)` : reexports crate-root et accès `crate::Item`.
## Hors périmètre
- aucune migration/index/schema ;
- aucun SQL PostgreSQL modifié ;
- aucune capability Store supplémentaire ;
- aucun payload/source byte dans les rows Observation ;
- aucun search/order DataTables arbitraire ;
- aucun cursor/pager applicatif.
## Gate recommandé
```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 visuel recommandé : `cargo tauri dev`, ouverture d'un détail Transaction puis Account, pagination des observations, chaînes longues/copie, fermeture/réouverture rapide pour vérifier l'absence de réponse stale.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Plan v0.3.8 — Store Desk V1 RAW
@@ -614,7 +614,11 @@ Les summaries d'observation restent strictement sans bytes RAW : clé d'observat
### pre.010 — observations UI + hardening UX
Intégrer provenance sûre dans les détails/tables retenues, aucune source payload byte, puis hardening des draws, counts, filtres, gros offsets et erreurs.
Intégrer les observations dans les modals de détail Transaction et Account via deux DataTables server-side dédiées, filtrées implicitement par l'identité exacte de l'entité ouverte. Les tables utilisent exclusivement les capabilities `RawTransactionObservationInspectionRead` / `RawAccountObservationInspectionRead` livrées en `pre.009`, avec ordre `descending` fixé côté application, `start/length -> offset/limit`, whitelist 25/50/100, counts exacts et DataTables comme seul propriétaire de pagination. Aucun cursor, network, search globale ou ordre DataTables arbitraire ne traverse l'IPC.
Les rows Observation projettent uniquement `RawAcquisitionProvenance` sûre : provider, protocol, acquisition method, origin, received/observed timestamps, endpoint/session/filter/commitment optionnels et hash/taille source. Account ajoute uniquement les metadata déjà admises (`is_startup`, `write_version`, transaction signature associée). Aucun source payload byte, payload RAW Transaction ni data Account n'est exposé. Les identifiants longs réutilisent le helper troncature/tooltip/copie sans valeur dans le tracing.
Le hardening V1 centralise la validation offset/limit côté Rust, rejette les draw/start/length hostiles avant IPC, refuse les counts dépassant `Number.MAX_SAFE_INTEGER`, ignore les réponses Observation devenues stale après changement/fermeture d'entité, ajuste les colonnes à l'ouverture du modal et conserve les erreurs d'observation dans une surface dédiée sans donnée métier.
### pre.011 — security / dependency / release completeness

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
<!-- version: 21 -->
<!-- version: 22 -->
# Validation v0.3.8 — Store Desk V1 RAW
@@ -748,3 +748,15 @@ Le gate opérateur de `0.3.8-pre.9` confirme que les audits KSP, `cargo check --
- [X] la version workspace devient `0.3.8-pre.9.fix.1` car du code Rust de production et un test Rust sont modifiés.
Le gate Cargo du fix reste à rejouer après application du delta.
## 34. `pre.010` — observations UI + hardening UX
Base directe : `0.3.8-pre.009-fix.001`, gate opérateur entièrement vert (`cargo check --workspace`, Clippy sans warning, Store API, PostgreSQL, Store façade et `--no-default-features`).
La tranche `pre.010` ajoute uniquement l'intégration application/UI des capabilities Observation existantes : deux DTO server-side backend-neutres, deux commands Tauri, deux chemins `StoreStartup`/`StoreRuntime` et deux DataTables intégrées aux détails Transaction/Account. Les observations sont filtrées par l'identité exacte déjà sélectionnée, en direction descending fixe. Aucun schéma, migration, SQL backend, Store API ou Store façade n'est modifié.
Les DTO Observation exposent seulement provenance sûre et metadata optionnelles Account ; aucun byte RAW/source n'est présent. Tous les nouveaux items `pub(crate)` sont réexportés via `lib.rs` et consommés avec `crate::Item`. Les chaînes longues utilisent le helper copyable existant et le tracing ne transporte ni signature, pubkey, state hash, observation key, source hash ni autre valeur copiée.
Hardening ajouté : validation Rust commune `inspection_page_from_request`, whitelist 25/50/100, offset `u64`, draw/start/length frontend contrôlés, counts décimaux refusés au-delà du domaine JS sûr, réponses Observation stale ignorées lors d'un changement d'entité, erreurs de query séparées dans chaque modal et ajustement DataTables au `shown.bs.modal`.
Validation d'assemblage assistant : audits Rust/exports/Markdown, syntaxe TypeScript/HTML, inventaire DTO/commands/Store paths et reconstruction delta. Les gates Cargo restent à exécuter par l'opérateur.