v0.3.8-pre.00-fix.001
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
74
deltas/0.3.8/pre.007-fix.001.md
Normal file
74
deltas/0.3.8/pre.007-fix.001.md
Normal 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)
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user