v0.3.8-pre.00-fix.001

This commit is contained in:
2026-09-03 18:45:53 +02:00
parent d5b4aef194
commit 32577ea48d
8 changed files with 291 additions and 17 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 458
# version: 459
[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.7"
version = "0.3.8-pre.7.fix.1"
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/sass/_app.scss
// version: 1
// version: 2
$app-header-height: 72px;
$app-footer-height: 42px;
@@ -90,3 +90,28 @@ body {
.app-store-table-container {
min-height: 240px;
}
.app-copyable-long-text {
align-items: center;
display: inline-flex;
gap: .375rem;
max-width: 100%;
min-width: 0;
}
.app-copyable-long-text__value {
cursor: help;
display: inline-block;
max-width: 24ch;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.app-copyable-long-text__copy {
flex: 0 0 auto;
line-height: 1;
padding: .25rem .375rem;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
// version: 4
// version: 5
import DataTable from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
@@ -50,6 +50,102 @@ const viewTitles: Record<ViewId, string> = {
let transactionTable: StoreDataTable | null = null;
let accountTable: StoreDataTable | null = null;
const COPY_FEEDBACK_MILLISECONDS = 1200;
const LONG_TEXT_HEAD_CHARACTERS = 12;
const LONG_TEXT_TAIL_CHARACTERS = 8;
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function truncateLongText(value: string): string {
const threshold = LONG_TEXT_HEAD_CHARACTERS + LONG_TEXT_TAIL_CHARACTERS + 1;
if (value.length <= threshold) {
return value;
}
return `${value.slice(0, LONG_TEXT_HEAD_CHARACTERS)}${value.slice(-LONG_TEXT_TAIL_CHARACTERS)}`;
}
function renderCopyableLongText(value: string, fieldId: string): string {
const escapedValue = escapeHtml(value);
const escapedVisible = escapeHtml(truncateLongText(value));
const escapedFieldId = escapeHtml(fieldId);
return `<span class="app-copyable-long-text"><span class="app-copyable-long-text__value font-monospace" title="${escapedValue}">${escapedVisible}</span><button class="btn btn-sm btn-outline-secondary app-copyable-long-text__copy" type="button" data-copy-long-text="${escapedValue}" data-copy-field="${escapedFieldId}" title="Copier la valeur complète" aria-label="Copier la valeur complète"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
}
function setCopyableLongText(elementId: string, value: string, fieldId: string): void {
const element = document.querySelector<HTMLElement>(`#${elementId}`);
if (!element) {
return;
}
element.replaceChildren();
const wrapper = document.createElement("span");
wrapper.className = "app-copyable-long-text";
const text = document.createElement("span");
text.className = "app-copyable-long-text__value font-monospace";
text.textContent = truncateLongText(value);
text.title = value;
const button = document.createElement("button");
button.className = "btn btn-sm btn-outline-secondary app-copyable-long-text__copy";
button.type = "button";
button.dataset.copyLongText = value;
button.dataset.copyField = fieldId;
button.title = "Copier la valeur complète";
button.setAttribute("aria-label", "Copier la valeur complète");
button.innerHTML = '<i class="fa-regular fa-copy" aria-hidden="true"></i>';
wrapper.append(text, button);
element.append(wrapper);
}
async function writeClipboardText(value: string): Promise<boolean> {
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
try {
await navigator.clipboard.writeText(value);
return true;
} catch {
// Fall back to the document copy command for WebViews without Clipboard API permission.
}
}
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.left = "-10000px";
textarea.style.top = "-10000px";
document.body.append(textarea);
textarea.select();
const copied = document.execCommand("copy");
textarea.remove();
return copied;
}
async function copyLongText(button: HTMLButtonElement, value: string, fieldId: string): Promise<void> {
frontendDebug("main", "Store Desk long text copy requested", { fieldId });
const copied = await writeClipboardText(value);
if (!copied) {
frontendError("main", "Store Desk long text copy failed", { fieldId });
return;
}
const icon = button.querySelector<HTMLElement>("i");
const previousClassName = icon?.className ?? "";
if (icon) {
icon.className = "fa-solid fa-check";
}
button.title = "Copié";
frontendDebug("main", "Store Desk long text copy completed", { fieldId });
window.setTimeout(() => {
if (icon) {
icon.className = previousClassName;
}
button.title = "Copier la valeur complète";
}, COPY_FEEDBACK_MILLISECONDS);
}
function isViewId(value: string | undefined): value is ViewId {
return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics";
}
@@ -187,20 +283,20 @@ function initializeTransactionTable(): void {
},
autoWidth: false,
columns: [
{ className: "font-monospace", data: "signature" },
{ data: "signature", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-signature") : String(data)) },
{ className: "font-monospace", data: "slotDecimal" },
{ data: "blockTimeUnixMillisDecimal", defaultContent: "—" },
{ data: "formatId" },
{ data: "formatVersion" },
{ data: "payloadSizeDecimal", defaultContent: "—" },
{ className: "font-monospace", data: "contentHash" },
{ data: "contentHash", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-content-hash") : String(data)) },
{ data: "retentionState", render: data => retentionBadge(String(data)) },
{
data: null,
defaultContent: "",
render: (_data, _type, row) => {
const transaction = row as StoreTransactionRowDto;
return `<button class="btn btn-sm btn-outline-primary" type="button" data-transaction-detail="${transaction.signature}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
return `<button class="btn btn-sm btn-outline-primary" type="button" data-transaction-detail="${escapeHtml(transaction.signature)}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
},
},
],
@@ -335,11 +431,11 @@ function clearTransactionDetail(): void {
}
function renderTransactionDetail(detail: StoreTransactionDetailDto): void {
setText("transactionDetailSignature", detail.signature);
setCopyableLongText("transactionDetailSignature", detail.signature, "transaction-detail-signature");
setText("transactionDetailSlot", detail.slotDecimal);
setText("transactionDetailBlockTime", detail.blockTimeUnixMillisDecimal ?? "—");
setText("transactionDetailFormat", `${detail.formatId} v${detail.formatVersion}`);
setText("transactionDetailContentHash", detail.contentHash);
setCopyableLongText("transactionDetailContentHash", detail.contentHash, "transaction-detail-content-hash");
setText("transactionDetailRetention", detail.retentionState);
setText("transactionDetailPayloadSize", detail.payloadSizeDecimal ?? "—");
setText("transactionDetailPayloadPreview", detail.payloadPreviewHex ?? "Payload non disponible dans cet état de rétention.");
@@ -352,7 +448,7 @@ async function openTransactionDetail(signature: string): Promise<void> {
return;
}
clearTransactionDetail();
setText("transactionDetailSignature", signature);
setCopyableLongText("transactionDetailSignature", signature, "transaction-detail-signature");
frontendDebug("main", "Store Desk transaction detail load requested");
const modal = Modal.getOrCreateInstance(modalElement);
modal.show();
@@ -410,6 +506,15 @@ function installInteractions(): void {
if (!(eventTarget instanceof Element)) {
return;
}
const copyButton = eventTarget.closest<HTMLButtonElement>("[data-copy-long-text]");
if (copyButton) {
const value = copyButton.dataset.copyLongText;
const fieldId = copyButton.dataset.copyField ?? "long-text";
if (value) {
void copyLongText(copyButton, value, fieldId);
}
return;
}
const detailButton = eventTarget.closest<HTMLElement>("[data-transaction-detail]");
if (detailButton) {
const signature = detailButton.dataset.transactionDetail;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
// version: 4
// version: 5
//! Desktop template and frontend contract tests for the Store Desk scaffold.
@@ -88,8 +88,7 @@ fn pre_007_shell_exposes_transaction_filters_detail_and_keeps_account_skeleton_u
}
assert!(html.contains("id=\"rawTransactionsTable\""));
assert!(html.contains("id=\"rawAccountsTable\""));
for overview_id in ["refreshOverview", "overviewStoreProfile", "overviewStoreTarget", "overviewStoreHealth", "overviewStorePool", "overviewStoreMigration"]
{
for overview_id in ["refreshOverview", "overviewStoreProfile", "overviewStoreTarget", "overviewStoreHealth", "overviewStorePool", "overviewStoreMigration"] {
assert!(html.contains(&format!("id=\"{overview_id}\"")), "missing Store Overview field: {overview_id}");
}
for transaction_id in ["refreshTransactions", "transactionSlotMin", "transactionSlotMax", "transactionDirection", "transactionDetailModal"] {
@@ -197,3 +196,20 @@ fn pre_007_transaction_ipc_maps_datatables_offset_limit_counts_and_detail_withou
assert!(!dto.contains("RawPageCursor"));
assert!(!frontend.contains("storePager"));
}
#[test]
fn pre_007_fix_001_long_transaction_identifiers_are_truncated_tooltipped_and_copyable() {
let frontend = include_str!("../frontend/ts/main.ts");
let sass = include_str!("../frontend/sass/_app.scss");
assert!(frontend.contains("truncateLongText"));
assert!(frontend.contains("renderCopyableLongText(String(data), "transaction-signature")"));
assert!(frontend.contains("renderCopyableLongText(String(data), "transaction-content-hash")"));
assert!(frontend.contains("setCopyableLongText("transactionDetailSignature""));
assert!(frontend.contains("setCopyableLongText("transactionDetailContentHash""));
assert!(frontend.contains("title="${escapedValue}""));
assert!(frontend.contains("data-copy-long-text"));
assert!(frontend.contains("navigator.clipboard.writeText"));
assert!(frontend.contains("document.execCommand("copy")"));
assert!(sass.contains(".app-copyable-long-text__value"));
assert!(sass.contains("text-overflow: ellipsis"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
// version: 5
// version: 6
//! Security boundary tests for the Store Desk scaffold.
@@ -128,3 +128,22 @@ fn pre_007_transaction_request_rejects_datatables_search_order_draw_and_physical
assert!(runtime.contains("\"ascending\" =>"));
assert!(runtime.contains("\"descending\" =>"));
}
#[test]
fn pre_007_fix_001_copy_tracing_never_logs_long_identifier_values() {
let frontend = include_str!("../frontend/ts/main.ts");
assert!(frontend.contains("Store Desk long text copy requested"));
assert!(frontend.contains("Store Desk long text copy completed"));
assert!(frontend.contains("Store Desk long text copy failed"));
assert!(frontend.contains("{ fieldId }"));
for forbidden in [
"copy requested", { value",
"copy completed", { value",
"copy failed", { value",
"copy requested", { signature",
"copy completed", { signature",
"copy failed", { signature",
] {
assert!(!frontend.contains(forbidden), "long identifier value leaked into copy tracing: {forbidden}");
}
}

View File

@@ -0,0 +1,74 @@
<!-- file: deltas/0.3.8/pre.007-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.8-pre.007-fix.001` — UX des identifiants longs
## Base requise
Base directe attendue : `0.3.8-pre.007` (`workspace.package.version = 0.3.8-pre.7`).
## Objet
Améliorer l'affichage des identifiants longs déjà exposés par RAW Transactions sans modifier le contrat Store ou IPC : troncature visuelle, valeur complète au survol et copie explicite.
## Comportement
Le frontend ajoute un helper générique qui :
- conserve 12 caractères de tête et 8 de fin pour les valeurs longues ;
- expose la valeur complète dans le tooltip natif `title` ;
- ajoute un bouton Copier adjacent ;
- utilise `navigator.clipboard.writeText` avec fallback local `document.execCommand("copy")` ;
- remplace temporairement l'icône Copier par une coche après succès.
Le helper est appliqué à :
```text
RAW Transactions / table : signature, content hash
RAW Transactions / détail : signature, content hash
```
Le mécanisme est générique afin d'être réutilisé en `pre.008` pour pubkey, owner, state hash et autres chaînes longues.
## Sécurité / tracing
Les événements de copie sont tracés sous forme :
```text
requested / completed / failed
fieldId logique uniquement
```
La valeur copiée n'est jamais journalisée. Aucun plugin clipboard Tauri, permission ou capability supplémentaire n'est ajouté.
## Hors scope
Aucun changement Store API, Store façade, PostgreSQL, SQL, migration, Config, command Tauri, DTO IPC ou contrat DataTables. RAW Accounts reste `serverSide:false` jusqu'à `pre.008`.
## Version
```text
delivery = 0.3.8-pre.007-fix.001
workspace.package.version = 0.3.8-pre.7.fix.1
commit = v0.3.8-pre.007-fix.001
tag = aucun
```
## Gate requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-store-desk
cargo test -p ksp-store-lib
cargo check -p ksp-store-lib --no-default-features
```
Smoke recommandé :
```bash
(cd crates/ksp-app-store-desk && cargo tauri dev)
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Plan v0.3.8 — Store Desk V1 RAW
@@ -588,6 +588,10 @@ Brancher l'unique pager DataTables via `ajax` function/Tauri invoke, mapping off
Implémentation retenue : `store_query_transactions` reçoit uniquement `offset`, `limit`, `slot_min`, `slot_max` et `direction`, dérive le network du Store déjà ouvert et appelle `RawTransactionInspectionRead`. Les counts restent des chaînes décimales jusqu'au contrôle `Number.MAX_SAFE_INTEGER` côté frontend. Les rows sont payload-free. `store_get_transaction_detail` reçoit uniquement la signature de la row, recharge l'entité via `RawTransactionRead`/`RawTransactionRetentionRead` et expose au maximum 512 bytes sous forme de preview hex, avec taille exacte et indicateur de troncature. `Full`, `Archived` et `Purged` sont distingués ; un `Purged` utilise son tombstone. Les Accounts restent `serverSide:false` jusqu'à `pre.008`.
#### pre.007-fix.001 — UX uniforme pour identifiants longs
Les signatures et content hashes déjà visibles dans la table Transactions et son modal sont affichés sous une forme tronquée au centre, avec la valeur complète disponible au survol via tooltip natif et un bouton de copie explicite. Le mécanisme reste générique et doit être réutilisé par `pre.008` pour les pubkeys, owners, state hashes et autres identifiants longs. Les actions de copie sont tracées uniquement par identifiant logique de champ ; la valeur copiée n'est jamais journalisée. Aucun plugin clipboard Tauri ni capability supplémentaire n'est ajouté : le frontend utilise l'API Clipboard du WebView avec fallback local `document.execCommand("copy")`.
### pre.008 — RawAccountState DataTables serverSide + détail
Même intégration pour Account, pubkey/range/direction, summaries JS-safe, détail data borné, responsive/scrollX/copy UX.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
<!-- version: 14 -->
<!-- version: 15 -->
# Validation v0.3.8 — Store Desk V1 RAW
@@ -576,5 +576,36 @@ cargo check -p ksp-store-lib --no-default-features: PASS
- [X] RAW Accounts reste volontairement `serverSide:false` jusqu'à `pre.008` ;
- [X] aucune modification `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, migration ou capability n'est introduite par cette tranche applicative.
Le gate Cargo opérateur de `pre.007` reste à exécuter après application du delta.
Le gate opérateur de `pre.007` est ensuite entièrement vert :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (314 table(s), 704 file(s))
cargo check --workspace: PASS
cargo clippy --workspace --all-targets: PASS
cargo test -p ksp-app-store-desk: PASS (21 unitaires + 4 dependency + 9 desktop contract + 7 security + 1 public API)
cargo test -p ksp-store-lib: PASS
cargo check -p ksp-store-lib --no-default-features: PASS
```
Le smoke `cargo tauri dev` confirme le chemin réel `serverSide` Transactions : chargement initial de 25 rows, navigation de page, passage à 100 rows/page, IPC `store_query_transactions`, maintien de RAW Accounts hors `serverSide` et shutdown Store borné.
## 29. `pre.007-fix.001` — troncature, tooltip et copie des identifiants longs
À la demande opérateur, la présentation des chaînes longues est harmonisée avant `pre.008` :
- [X] signature de transaction tronquée au centre dans la table ;
- [X] content hash tronqué au centre dans la table ;
- [X] signature et content hash du modal utilisent la même présentation ;
- [X] la valeur complète reste accessible au survol par attribut `title` ;
- [X] chaque valeur dispose d'un bouton Copier explicite ;
- [X] Clipboard API utilisée en premier, avec fallback `document.execCommand("copy")` sans plugin Tauri supplémentaire ;
- [X] feedback visuel temporaire après copie ;
- [X] tracing `requested/completed/failed` ne transporte que le `fieldId`, jamais la signature, le hash ou la valeur copiée ;
- [X] le helper reste générique pour réutilisation directe en `pre.008` sur pubkey/owner/state hash ;
- [X] aucun changement backend, IPC, Store, SQL, Config ou capability Tauri.
Le gate Cargo de ce fix reste à rejouer après application du delta.