diff --git a/Cargo.toml b/Cargo.toml
index a0b9c7a..e9f575f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/crates/ksp-app-store-desk/frontend/main.html b/crates/ksp-app-store-desk/frontend/main.html
index 7490e0a..acd8d58 100644
--- a/crates/ksp-app-store-desk/frontend/main.html
+++ b/crates/ksp-app-store-desk/frontend/main.html
@@ -1,5 +1,5 @@
-
+
@@ -249,6 +249,25 @@
—
+
+
+
+
Observations d’acquisition
+
Provenance sûre uniquement ; aucun payload source n’est chargé.
+
+
+
+
+
+
+
+ | Received | Observed | Provider | Protocol | Method | Origin | Endpoint | Commitment | Session | Filter | Source hash | Source bytes | Observation key |
+
+
+
+
+
+
@@ -284,6 +303,25 @@
—
+
+
+
+
Observations d’acquisition
+
Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account n’est chargé.
+
+
+
+
+
+
+
+ | Received | Observed | Provider | Protocol | Method | Origin | Endpoint | Commitment | Session | Filter | Source hash | Source bytes | Observation key | Startup | Write version | Tx signature |
+
+
+
+
+
+
diff --git a/crates/ksp-app-store-desk/frontend/ts/main.ts b/crates/ksp-app-store-desk/frontend/ts/main.ts
index 9d21e24..defaf24 100644
--- a/crates/ksp-app-store-desk/frontend/ts/main.ts
+++ b/crates/ksp-app-store-desk/frontend/ts/main.ts
@@ -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 = {
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 `${escapeHtml(origin)}`;
+}
+
+function renderOptionalBoolean(value: boolean | null): string {
+ if (value === null) {
+ return "—";
+ }
+ return value ? 'oui' : 'non';
+}
+
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 {
+ return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
+}
+
+function emptyAccountObservationPage(draw: number): DataTablesPageResponse {
+ return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
+}
+
+function setTransactionObservationError(message: string | null): void {
+ const element = document.querySelector("#transactionObservationsError");
+ if (!element) {
+ return;
+ }
+ element.hidden = message === null;
+ element.textContent = message ?? "";
+}
+
+function setAccountObservationError(message: string | null): void {
+ const element = document.querySelector("#accountObservationsError");
+ if (!element) {
+ return;
+ }
+ element.hidden = message === null;
+ element.textContent = message ?? "";
+}
+
+async function queryTransactionObservations(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse) => void): Promise {
+ 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("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) => void): Promise {
+ 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("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 {
}
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("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 {
try {
const detail = await invokeKsp("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("#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("#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 {
frontendInfo("main", "Store Desk main frontend loaded");
initializeTransactionTable();
initializeAccountTable();
+ initializeTransactionObservationTable();
+ initializeAccountObservationTable();
installInteractions();
activateView("overview");
await refreshDiagnostics();
diff --git a/crates/ksp-app-store-desk/src/app_state.rs b/crates/ksp-app-store-desk/src/app_state.rs
index 4f8a097..31a7767 100644
--- a/crates/ksp-app-store-desk/src/app_state.rs
+++ b/crates/ksp-app-store-desk/src/app_state.rs
@@ -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 {
+ 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 {
+ 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 {
diff --git a/crates/ksp-app-store-desk/src/dto_observation.rs b/crates/ksp-app-store-desk/src/dto_observation.rs
new file mode 100644
index 0000000..5257c56
--- /dev/null
+++ b/crates/ksp-app-store-desk/src/dto_observation.rs
@@ -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,
+ /// Optional commitment code captured at acquisition.
+ pub(crate) commitment: std::option::Option,
+ /// Optional Config-owned logical endpoint identifier.
+ pub(crate) endpoint_id: std::option::Option,
+ /// Optional bounded logical filter identifier.
+ pub(crate) filter_id: std::option::Option,
+ /// Optional source-observation timestamp encoded as exact Unix-millisecond decimal text.
+ pub(crate) observed_at_unix_millis_decimal: std::option::Option,
+ /// 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,
+ /// Optional source-wire payload size encoded as exact decimal text.
+ pub(crate) source_payload_size_decimal: std::option::Option,
+}
+
+/// 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,
+}
+
+/// 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,
+ /// 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,
+ /// Optional source-specific account write version encoded as exact decimal text.
+ pub(crate) write_version_decimal: std::option::Option,
+}
+
+/// 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,
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/dto_observation.rs"]
+mod tests;
diff --git a/crates/ksp-app-store-desk/src/lib.rs b/crates/ksp-app-store-desk/src/lib.rs
index 16dad20..0a1adf9 100644
--- a/crates/ksp-app-store-desk/src/lib.rs
+++ b/crates/ksp-app-store-desk/src/lib.rs
@@ -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.
diff --git a/crates/ksp-app-store-desk/src/store_runtime.rs b/crates/ksp-app-store-desk/src/store_runtime.rs
index 8264b9c..60d922e 100644
--- a/crates/ksp-app-store-desk/src/store_runtime.rs
+++ b/crates/ksp-app-store-desk/src/store_runtime.rs
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
- 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 {
+ 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::();
+ let offset = offset_text.parse::();
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 {
+ 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 {
+ 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 {
+ 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 {
- 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::();
- 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));
}
diff --git a/crates/ksp-app-store-desk/src/tauri.rs b/crates/ksp-app-store-desk/src/tauri.rs
index 9e32be8..e231d9a 100644
--- a/crates/ksp-app-store-desk/src/tauri.rs
+++ b/crates/ksp-app-store-desk/src/tauri.rs
@@ -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::Builder) -> std::result::Re
};
}
+#[tauri::command]
+async fn store_query_account_observations(
+ request: crate::StoreAccountObservationQueryRequestDto,
+ state: tauri::State<'_, crate::AppState>,
+) -> std::result::Result {
+ 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 {
+ 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,
diff --git a/crates/ksp-app-store-desk/tests/dependency_boundary.rs b/crates/ksp-app-store-desk/tests/dependency_boundary.rs
index 25a7db6..0db6906 100644
--- a/crates/ksp-app-store-desk/tests/dependency_boundary.rs
+++ b/crates/ksp-app-store-desk/tests/dependency_boundary.rs
@@ -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}");
+ }
+}
diff --git a/crates/ksp-app-store-desk/tests/desktop_contract.rs b/crates/ksp-app-store-desk/tests/desktop_contract.rs
index 1b6d645..89ca664 100644
--- a/crates/ksp-app-store-desk/tests/desktop_contract.rs
+++ b/crates/ksp-app-store-desk/tests/desktop_contract.rs
@@ -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"));
+}
diff --git a/crates/ksp-app-store-desk/tests/desktop_security.rs b/crates/ksp-app-store-desk/tests/desktop_security.rs
index 406fa48..4a4ac34 100644
--- a/crates/ksp-app-store-desk/tests/desktop_security.rs
+++ b/crates/ksp-app-store-desk/tests/desktop_security.rs
@@ -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::()"));
+}
diff --git a/crates/ksp-app-store-desk/unit_tests/dto_observation.rs b/crates/ksp-app-store-desk/unit_tests/dto_observation.rs
new file mode 100644
index 0000000..2258875
--- /dev/null
+++ b/crates/ksp-app-store-desk/unit_tests/dto_observation.rs
@@ -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}");
+ }
+}
diff --git a/deltas/0.3.8/pre.010.md b/deltas/0.3.8/pre.010.md
new file mode 100644
index 0000000..022c2be
--- /dev/null
+++ b/deltas/0.3.8/pre.010.md
@@ -0,0 +1,52 @@
+
+
+
+# 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.
diff --git a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md
index 99b28e0..ba51c93 100644
--- a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md
+++ b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md
@@ -1,5 +1,5 @@
-
+
# 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
diff --git a/docs/validation/025-V0_3_8_STORE_DESK.md b/docs/validation/025-V0_3_8_STORE_DESK.md
index a5dcdc5..2c5945c 100644
--- a/docs/validation/025-V0_3_8_STORE_DESK.md
+++ b/docs/validation/025-V0_3_8_STORE_DESK.md
@@ -1,5 +1,5 @@
-
+
# 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.