v0.2.12-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 293
|
||||
# version: 294
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.12-pre.6"
|
||||
version = "0.2.12-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -44,13 +44,16 @@
|
||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<section data-view-panel="prices">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Prices</h1>
|
||||
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Rafraîchissement individuel, sélectionné ou global, sans polling ni fallback automatique.</p>
|
||||
<p class="text-body-secondary mb-0">Inventaire provider-neutral SOL/USD. Les observations restent distinctes : aucune moyenne, aucun fallback automatique et aucun polling.</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span id="selectedProviderCount" class="small text-body-secondary">0 selected</span>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap app-market-toolbar">
|
||||
<span id="selectedProviderCount" class="small text-body-secondary" aria-live="polite">0 selected</span>
|
||||
<button id="clearSelectionButton" class="btn btn-sm btn-outline-secondary" type="button" disabled>
|
||||
<i class="fa-solid fa-xmark me-1" aria-hidden="true"></i>Clear selection
|
||||
</button>
|
||||
<button id="refreshSelectedButton" class="btn btn-sm btn-outline-primary" type="button" disabled>
|
||||
<i class="fa-solid fa-list-check me-1" aria-hidden="true"></i>Refresh selected
|
||||
</button>
|
||||
@@ -59,6 +62,40 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pricesSummary" class="row g-2 mb-3" aria-label="Résumé des providers">
|
||||
<div class="col-6 col-lg">
|
||||
<div class="app-market-summary-card">
|
||||
<span class="app-market-summary-label">Providers</span>
|
||||
<strong id="pricesSummaryProviders">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="app-market-summary-card">
|
||||
<span class="app-market-summary-label">Ready</span>
|
||||
<strong id="pricesSummaryReady">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="app-market-summary-card">
|
||||
<span class="app-market-summary-label">Observed</span>
|
||||
<strong id="pricesSummaryObserved">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="app-market-summary-card">
|
||||
<span class="app-market-summary-label">Attention</span>
|
||||
<strong id="pricesSummaryAttention">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg">
|
||||
<div class="app-market-summary-card">
|
||||
<span class="app-market-summary-label">Refreshing</span>
|
||||
<strong id="pricesSummaryLoading">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pricesLoading" class="app-placeholder">
|
||||
<div>
|
||||
<i class="fa-solid fa-spinner fa-spin text-body-secondary mb-3" aria-hidden="true"></i>
|
||||
@@ -73,22 +110,24 @@
|
||||
<p class="text-body-secondary mb-0">Le runtime Off-chain ne contient actuellement aucune ligne provider.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pricesDiagnostic" class="alert alert-warning mb-3" hidden></div>
|
||||
<div id="pricesTableContainer" class="table-responsive" hidden>
|
||||
<table id="marketPriceTable" class="table table-hover align-middle mb-0">
|
||||
<div id="pricesDiagnostic" class="alert alert-warning mb-3" role="status" aria-live="polite" hidden></div>
|
||||
<div id="pricesTableContainer" class="table-responsive app-market-table-container" hidden>
|
||||
<table id="marketPriceTable" class="table table-hover align-middle mb-0 app-market-table">
|
||||
<caption class="visually-hidden">Providers SOL/USD, disponibilité, prix exacts, fraîcheur et actions de refresh.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="visually-hidden">Select</span></th>
|
||||
<th scope="col" class="app-market-selection-column">
|
||||
<input id="selectAllProviders" class="form-check-input" type="checkbox" aria-label="Select all providers" disabled>
|
||||
</th>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">Pair</th>
|
||||
<th scope="col">Semantics</th>
|
||||
<th scope="col">Auth</th>
|
||||
<th scope="col" class="d-none d-xl-table-cell">Semantics</th>
|
||||
<th scope="col" class="d-none d-xl-table-cell">Auth</th>
|
||||
<th scope="col">Availability</th>
|
||||
<th scope="col">Exact price</th>
|
||||
<th scope="col">Provider timestamp (ms)</th>
|
||||
<th scope="col">Received at (ms)</th>
|
||||
<th scope="col">Retry at (ms)</th>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col" class="app-market-time-column">Observation times</th>
|
||||
<th scope="col" class="app-market-time-column">Retry</th>
|
||||
<th scope="col" class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="marketPriceRows"></tbody>
|
||||
@@ -127,7 +166,7 @@
|
||||
<dt class="col-sm-5">Fallback Logging</dt>
|
||||
<dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
||||
</dl>
|
||||
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" hidden></div>
|
||||
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/frontend/sass/_app.scss
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
@@ -61,7 +61,10 @@ body {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.app-runtime-list dd {
|
||||
.app-runtime-list dd,
|
||||
.app-market-code,
|
||||
.app-market-price,
|
||||
.app-market-time-exact {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
@@ -103,3 +106,130 @@ body {
|
||||
.app-placeholder i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.app-market-toolbar {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.app-market-summary-card {
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
|
||||
.app-market-summary-card strong {
|
||||
min-width: 2ch;
|
||||
font-size: 1.15rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.app-market-summary-label {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.app-market-table-container {
|
||||
max-height: min(62vh, 720px);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.app-market-table {
|
||||
min-width: 1080px;
|
||||
}
|
||||
|
||||
.app-market-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--bs-tertiary-bg);
|
||||
box-shadow: inset 0 -1px 0 var(--bs-border-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-market-table tbody tr[data-availability="ready"] {
|
||||
--bs-table-accent-bg: rgba($success, 0.025);
|
||||
}
|
||||
|
||||
.app-market-table tbody tr[data-availability="cooling_down"],
|
||||
.app-market-table tbody tr[data-availability="temporarily_unavailable"] {
|
||||
--bs-table-accent-bg: rgba($warning, 0.04);
|
||||
}
|
||||
|
||||
.app-market-table tbody tr[data-availability="authentication_unavailable"],
|
||||
.app-market-table tbody tr[data-availability="misconfigured"] {
|
||||
--bs-table-accent-bg: rgba($danger, 0.035);
|
||||
}
|
||||
|
||||
.app-market-selection-column {
|
||||
width: 42px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-market-time-column {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.app-market-provider-name,
|
||||
.app-market-price {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-market-code,
|
||||
.app-market-time-exact {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.app-market-price {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-market-time {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.app-market-time-line {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.35rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.app-market-time-label {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-market-time-value {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-market-row-loading {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.app-sidebar {
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
max-width: 190px;
|
||||
}
|
||||
|
||||
.app-market-toolbar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -17,11 +17,46 @@ installFrontendConsoleBridge("main");
|
||||
|
||||
type ViewId = "prices" | "diagnostics";
|
||||
|
||||
type AvailabilityPresentation = {
|
||||
badgeClass: string;
|
||||
iconClass: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const viewTitles: Record<ViewId, string> = {
|
||||
prices: "Prices",
|
||||
diagnostics: "Diagnostics",
|
||||
};
|
||||
|
||||
const availabilityPresentations: Record<string, AvailabilityPresentation> = {
|
||||
authentication_unavailable: { badgeClass: "text-bg-danger", iconClass: "fa-key", label: "Authentication unavailable" },
|
||||
cooling_down: { badgeClass: "text-bg-warning", iconClass: "fa-clock", label: "Cooling down" },
|
||||
disabled: { badgeClass: "text-bg-secondary", iconClass: "fa-ban", label: "Disabled" },
|
||||
misconfigured: { badgeClass: "text-bg-danger", iconClass: "fa-triangle-exclamation", label: "Misconfigured" },
|
||||
quota_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-gauge-high", label: "Quota unavailable" },
|
||||
ready: { badgeClass: "text-bg-success", iconClass: "fa-circle-check", label: "Ready" },
|
||||
temporarily_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-cloud-arrow-down", label: "Temporarily unavailable" },
|
||||
unknown: { badgeClass: "text-bg-secondary", iconClass: "fa-circle-question", label: "Unknown" },
|
||||
};
|
||||
|
||||
const authLabels: Record<string, string> = {
|
||||
none: "Keyless",
|
||||
optional_api_key: "Optional API key",
|
||||
required_api_key: "API key required",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
|
||||
const semanticsLabels: Record<string, string> = {
|
||||
aggregated_market: "Aggregated market",
|
||||
dex_pair_usd: "DEX pair USD",
|
||||
exchange_last_trade: "Exchange last trade",
|
||||
solana_heuristic: "Solana heuristic",
|
||||
solana_spot: "Solana spot",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
|
||||
const jsDateMaxUnixMillis = 8_640_000_000_000_000n;
|
||||
|
||||
let currentMarketPriceRows: MarketPriceProviderRowDto[] = [];
|
||||
const selectedProviderIds = new Set<string>();
|
||||
|
||||
@@ -59,10 +94,14 @@ function bindNavigation(): void {
|
||||
frontendTrace("main", "SOL Prices Desk navigation handlers installed");
|
||||
}
|
||||
|
||||
function appendMarketPriceCell(row: HTMLTableRowElement, value: string): void {
|
||||
function appendMarketPriceCell(row: HTMLTableRowElement, value: string, className?: string): HTMLTableCellElement {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = value;
|
||||
if (className) {
|
||||
cell.className = className;
|
||||
}
|
||||
row.append(cell);
|
||||
return cell;
|
||||
}
|
||||
|
||||
function clearPricesDiagnostic(): void {
|
||||
@@ -94,21 +133,49 @@ function synchronizeSelectedProviders(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary(): void {
|
||||
const summaryValues: Record<string, number> = {
|
||||
pricesSummaryProviders: currentMarketPriceRows.length,
|
||||
pricesSummaryReady: currentMarketPriceRows.filter(provider => provider.availability === "ready" && !provider.loading).length,
|
||||
pricesSummaryObserved: currentMarketPriceRows.filter(provider => provider.refreshed).length,
|
||||
pricesSummaryAttention: currentMarketPriceRows.filter(provider => provider.availability !== "ready").length,
|
||||
pricesSummaryLoading: currentMarketPriceRows.filter(provider => provider.loading).length,
|
||||
};
|
||||
for (const [elementId, value] of Object.entries(summaryValues)) {
|
||||
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||
if (element) {
|
||||
element.textContent = value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function synchronizeBatchControls(): void {
|
||||
const selectedCount = selectedProviderIdsInRowOrder().length;
|
||||
const providerCount = currentMarketPriceRows.length;
|
||||
const anyLoading = currentMarketPriceRows.some(provider => provider.loading);
|
||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||
const selectedCountLabel = document.querySelector<HTMLElement>("#selectedProviderCount");
|
||||
if (selectedButton) {
|
||||
selectedButton.disabled = selectedCount === 0 || anyLoading;
|
||||
}
|
||||
if (allButton) {
|
||||
allButton.disabled = currentMarketPriceRows.length === 0 || anyLoading;
|
||||
allButton.disabled = providerCount === 0 || anyLoading;
|
||||
}
|
||||
if (clearButton) {
|
||||
clearButton.disabled = selectedCount === 0 || anyLoading;
|
||||
}
|
||||
if (selectAll) {
|
||||
selectAll.disabled = providerCount === 0 || anyLoading;
|
||||
selectAll.checked = providerCount !== 0 && selectedCount === providerCount;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < providerCount;
|
||||
}
|
||||
if (selectedCountLabel) {
|
||||
selectedCountLabel.textContent = `${selectedCount} selected`;
|
||||
}
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function markProvidersLoading(providerIds: string[]): void {
|
||||
@@ -198,6 +265,7 @@ async function refreshAllMarketPrices(): Promise<void> {
|
||||
|
||||
function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.className = "app-market-selection-column";
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "form-check-input";
|
||||
@@ -220,13 +288,152 @@ function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProv
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendProviderCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
const name = document.createElement("div");
|
||||
name.className = "app-market-provider-name";
|
||||
name.textContent = provider.displayName;
|
||||
const code = document.createElement("div");
|
||||
code.className = "app-market-code";
|
||||
code.textContent = provider.providerId;
|
||||
cell.append(name, code);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendBadgeCell(row: HTMLTableRowElement, label: string, badgeClass = "text-bg-light", className?: string): void {
|
||||
const cell = document.createElement("td");
|
||||
if (className) {
|
||||
cell.className = className;
|
||||
}
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `badge ${badgeClass}`;
|
||||
badge.textContent = label;
|
||||
cell.append(badge);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function availabilityPresentation(provider: MarketPriceProviderRowDto): AvailabilityPresentation {
|
||||
if (provider.loading) {
|
||||
return { badgeClass: "text-bg-info", iconClass: "fa-spinner fa-spin", label: "Refreshing" };
|
||||
}
|
||||
return availabilityPresentations[provider.availability] ?? availabilityPresentations.unknown;
|
||||
}
|
||||
|
||||
function appendAvailabilityCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
const presentation = availabilityPresentation(provider);
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `badge ${presentation.badgeClass}`;
|
||||
const icon = document.createElement("i");
|
||||
icon.className = `fa-solid ${presentation.iconClass} me-1`;
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
badge.append(icon, document.createTextNode(presentation.label));
|
||||
cell.append(badge);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendPriceCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
if (provider.price === null) {
|
||||
const empty = document.createElement("span");
|
||||
empty.className = "text-body-secondary";
|
||||
empty.textContent = "No observation";
|
||||
cell.append(empty);
|
||||
} else {
|
||||
const price = document.createElement("span");
|
||||
price.className = "app-market-price";
|
||||
price.textContent = provider.price;
|
||||
const currency = document.createElement("small");
|
||||
currency.className = "text-body-secondary ms-1";
|
||||
currency.textContent = "USD";
|
||||
cell.append(price, currency);
|
||||
}
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function formatUnixMillis(value: string): { exact: string; iso: string; local: string } | null {
|
||||
try {
|
||||
const exact = BigInt(value);
|
||||
if (exact < -jsDateMaxUnixMillis || exact > jsDateMaxUnixMillis) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(Number(exact));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
exact: value,
|
||||
iso: date.toISOString(),
|
||||
local: date.toLocaleString("fr-CH", { dateStyle: "short", timeStyle: "medium", hour12: false }),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function appendTimestampValue(container: HTMLElement, label: string, value: string | null, emptyLabel: string): void {
|
||||
const line = document.createElement("div");
|
||||
line.className = "app-market-time-line";
|
||||
const prefix = document.createElement("span");
|
||||
prefix.className = "app-market-time-label";
|
||||
prefix.textContent = label;
|
||||
const valueContainer = document.createElement("span");
|
||||
valueContainer.className = "app-market-time-value";
|
||||
if (value === null) {
|
||||
valueContainer.classList.add("text-body-secondary");
|
||||
valueContainer.textContent = emptyLabel;
|
||||
} else {
|
||||
const formatted = formatUnixMillis(value);
|
||||
if (formatted === null) {
|
||||
const exact = document.createElement("span");
|
||||
exact.className = "app-market-time-exact";
|
||||
exact.textContent = `${value} ms`;
|
||||
valueContainer.append(exact);
|
||||
} else {
|
||||
const time = document.createElement("time");
|
||||
time.dateTime = formatted.iso;
|
||||
time.textContent = formatted.local;
|
||||
time.title = formatted.iso;
|
||||
const exact = document.createElement("span");
|
||||
exact.className = "app-market-time-exact d-block";
|
||||
exact.textContent = `${formatted.exact} ms`;
|
||||
valueContainer.append(time, exact);
|
||||
}
|
||||
}
|
||||
line.append(prefix, valueContainer);
|
||||
container.append(line);
|
||||
}
|
||||
|
||||
function appendObservationTimeCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.className = "app-market-time-column";
|
||||
const time = document.createElement("div");
|
||||
time.className = "app-market-time";
|
||||
appendTimestampValue(time, "Provider", provider.providerTimestampUnixMillis, "Not supplied");
|
||||
appendTimestampValue(time, "KSP", provider.receivedAtUnixMillis, "—");
|
||||
cell.append(time);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendRetryCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.className = "app-market-time-column";
|
||||
const time = document.createElement("div");
|
||||
time.className = "app-market-time";
|
||||
appendTimestampValue(time, "Retry", provider.retryAtUnixMillis, "—");
|
||||
cell.append(time);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.className = "text-end";
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "btn btn-sm btn-outline-primary text-nowrap";
|
||||
button.disabled = provider.loading;
|
||||
button.dataset.providerId = provider.providerId;
|
||||
button.setAttribute("aria-label", `Refresh ${provider.displayName}`);
|
||||
button.innerHTML = provider.loading
|
||||
? '<i class="fa-solid fa-spinner fa-spin me-1" aria-hidden="true"></i>Refreshing…'
|
||||
: '<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>Refresh';
|
||||
@@ -257,34 +464,68 @@ function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
|
||||
for (const provider of rows) {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.providerId = provider.providerId;
|
||||
row.dataset.availability = provider.availability;
|
||||
row.classList.toggle("app-market-row-loading", provider.loading);
|
||||
appendSelectionCell(row, provider);
|
||||
appendMarketPriceCell(row, provider.displayName);
|
||||
appendMarketPriceCell(row, provider.pair);
|
||||
appendMarketPriceCell(row, provider.semantics);
|
||||
appendMarketPriceCell(row, provider.authMode);
|
||||
appendMarketPriceCell(row, provider.loading ? "loading" : provider.availability);
|
||||
appendMarketPriceCell(row, provider.price ?? "—");
|
||||
appendMarketPriceCell(row, provider.providerTimestampUnixMillis ?? "—");
|
||||
appendMarketPriceCell(row, provider.receivedAtUnixMillis ?? "—");
|
||||
appendMarketPriceCell(row, provider.retryAtUnixMillis ?? "—");
|
||||
appendProviderCell(row, provider);
|
||||
appendBadgeCell(row, provider.pair, "text-bg-primary");
|
||||
appendBadgeCell(row, semanticsLabels[provider.semantics] ?? semanticsLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||
appendBadgeCell(row, authLabels[provider.authMode] ?? authLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||
appendAvailabilityCell(row, provider);
|
||||
appendPriceCell(row, provider);
|
||||
appendObservationTimeCell(row, provider);
|
||||
appendRetryCell(row, provider);
|
||||
appendRefreshCell(row, provider);
|
||||
tableBody.append(row);
|
||||
}
|
||||
synchronizeBatchControls();
|
||||
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", { rowCount: rows.length });
|
||||
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", {
|
||||
rowCount: rows.length,
|
||||
observedCount: rows.filter(provider => provider.refreshed).length,
|
||||
attentionCount: rows.filter(provider => provider.availability !== "ready").length,
|
||||
});
|
||||
}
|
||||
|
||||
function selectAllProviders(selected: boolean): void {
|
||||
selectedProviderIds.clear();
|
||||
if (selected) {
|
||||
for (const provider of currentMarketPriceRows) {
|
||||
selectedProviderIds.add(provider.providerId);
|
||||
}
|
||||
}
|
||||
frontendDebug("main", "SOL Prices Desk select-all providers control changed", {
|
||||
selected,
|
||||
selectedCount: selectedProviderIds.size,
|
||||
});
|
||||
renderMarketPriceRows(currentMarketPriceRows);
|
||||
}
|
||||
|
||||
function clearProviderSelection(): void {
|
||||
const selectedCount = selectedProviderIds.size;
|
||||
frontendDebug("main", "SOL Prices Desk clear provider selection control clicked", { selectedCount });
|
||||
selectedProviderIds.clear();
|
||||
renderMarketPriceRows(currentMarketPriceRows);
|
||||
}
|
||||
|
||||
function bindMarketPriceControls(): void {
|
||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||
selectedButton?.addEventListener("click", () => {
|
||||
void refreshSelectedMarketPrices();
|
||||
});
|
||||
allButton?.addEventListener("click", () => {
|
||||
void refreshAllMarketPrices();
|
||||
});
|
||||
clearButton?.addEventListener("click", () => {
|
||||
clearProviderSelection();
|
||||
});
|
||||
selectAll?.addEventListener("change", () => {
|
||||
selectAllProviders(selectAll.checked);
|
||||
});
|
||||
synchronizeBatchControls();
|
||||
frontendTrace("main", "SOL Prices Desk market-price batch controls installed");
|
||||
frontendTrace("main", "SOL Prices Desk market-price controls installed");
|
||||
}
|
||||
|
||||
function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-solprices-desk",
|
||||
"private": true,
|
||||
"version": "0.2.12-pre.6",
|
||||
"version": "0.2.12-pre.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/app_state.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Shared backend state owned by the SOL Prices Desk Tauri application.
|
||||
|
||||
@@ -129,7 +129,7 @@ impl crate::AppState {
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
provider_count,
|
||||
ready_provider_count,
|
||||
shell_phase: "pre.006-multi-refresh".to_owned(),
|
||||
shell_phase: "pre.007-ux-instrumentation".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
unavailable_provider_count,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/tauri.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Tauri runtime assembly for the KSP SOL prices desktop application.
|
||||
|
||||
@@ -98,12 +98,24 @@ fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri:
|
||||
});
|
||||
}
|
||||
|
||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = domain,
|
||||
command = command,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"SOL Prices Desk Tauri command failed"
|
||||
);
|
||||
return crate::CommandErrorDto::from_error(error);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,7 +124,7 @@ fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::
|
||||
let result = state.runtime_status();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,10 +135,17 @@ fn list_market_prices(
|
||||
let result = state.list_market_prices();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, row_count = value.len(), "projected SOL Prices Desk market-price registry rows");
|
||||
ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||
row_count = value.len(),
|
||||
"projected SOL Prices Desk market-price registry rows"
|
||||
);
|
||||
std::result::Result::Ok(value)
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(project_command_error("list_market_prices", crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,7 +156,9 @@ async fn refresh_all_market_prices(
|
||||
let result = state.refresh_all_market_prices().await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(project_command_error("refresh_all_market_prices", crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,7 +170,9 @@ async fn refresh_market_price(
|
||||
let result = state.refresh_market_price(provider_id).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(project_command_error("refresh_market_price", crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,7 +184,9 @@ async fn refresh_market_prices(
|
||||
let result = state.refresh_market_prices(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(project_command_error("refresh_market_prices", crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,6 +199,6 @@ async fn splash_frontend_ready(
|
||||
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP SOL Prices Desk",
|
||||
"version": "0.2.12-pre.6",
|
||||
"version": "0.2.12-pre.7",
|
||||
"identifier": "com.sasedev.ksp-app-solprices-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
|
||||
|
||||
@@ -246,3 +246,46 @@ fn pre_006_market_price_runtime_adds_selected_and_global_refresh_without_polling
|
||||
assert!(!main_typescript.contains(forbidden), "pre.006 must not add consumer scheduling: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_prices_view_is_responsive_and_projects_generic_states_without_price_coercion() {
|
||||
let root = app_root();
|
||||
let main_html = read_text(root.join("frontend/main.html").as_path());
|
||||
for required in [
|
||||
r#"id="pricesSummaryProviders""#,
|
||||
r#"id="pricesSummaryReady""#,
|
||||
r#"id="pricesSummaryObserved""#,
|
||||
r#"id="pricesSummaryAttention""#,
|
||||
r#"id="pricesSummaryLoading""#,
|
||||
r#"id="selectAllProviders""#,
|
||||
r#"id="clearSelectionButton""#,
|
||||
"Observation times",
|
||||
"app-market-table-container",
|
||||
"visually-hidden",
|
||||
] {
|
||||
assert!(main_html.contains(required), "pre.007 Prices UX is missing {required}");
|
||||
}
|
||||
let main_typescript = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"availabilityPresentations",
|
||||
"Authentication unavailable",
|
||||
"Cooling down",
|
||||
"Quota unavailable",
|
||||
"Temporarily unavailable",
|
||||
"formatUnixMillis",
|
||||
"BigInt(value)",
|
||||
"Not supplied",
|
||||
"selectAllProviders",
|
||||
"clearProviderSelection",
|
||||
"pricesSummaryObserved",
|
||||
"app-market-row-loading",
|
||||
] {
|
||||
assert!(main_typescript.contains(required), "pre.007 Prices UX is missing {required}");
|
||||
}
|
||||
assert!(!main_typescript.contains("parseFloat("));
|
||||
assert!(!main_typescript.contains("Number(provider.price"));
|
||||
let app_scss = read_text(root.join("frontend/sass/_app.scss").as_path());
|
||||
for required in ["position: sticky", "font-variant-numeric: tabular-nums", "app-market-summary-card", "app-market-table-container"] {
|
||||
assert!(app_scss.contains(required), "pre.007 responsive styling is missing {required}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_security.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Security, ownership and frontend instrumentation canaries for SOL Prices Desk.
|
||||
|
||||
@@ -142,3 +142,47 @@ fn pre_006_frontend_batch_controls_are_logged_without_price_or_provider_payloads
|
||||
assert!(!main.contains(forbidden), "batch frontend logging must not serialize price/provider/secret values: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_all_frontend_controls_and_backend_command_failures_are_instrumented_safely() {
|
||||
let root = app_root();
|
||||
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"SOL Prices Desk navigation control clicked",
|
||||
"SOL Prices Desk market-price selection control changed",
|
||||
"SOL Prices Desk select-all providers control changed",
|
||||
"SOL Prices Desk clear provider selection control clicked",
|
||||
"SOL Prices Desk market-price refresh control clicked",
|
||||
"SOL Prices Desk selected market-price refresh control clicked",
|
||||
"SOL Prices Desk global market-price refresh control clicked",
|
||||
] {
|
||||
assert!(main.contains(required), "pre.007 frontend control instrumentation is missing {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"JSON.stringify(currentMarketPriceRows)",
|
||||
"JSON.stringify(result)",
|
||||
"price: provider.price",
|
||||
"providerTimestampUnixMillis:",
|
||||
"receivedAtUnixMillis:",
|
||||
"retryAtUnixMillis:",
|
||||
"apiKey",
|
||||
"authorization",
|
||||
] {
|
||||
assert!(!main.contains(forbidden), "pre.007 frontend instrumentation must not log business/secret values: {forbidden}");
|
||||
}
|
||||
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("fn project_command_error"));
|
||||
assert!(tauri.contains("error_domain = error.code().domain()"));
|
||||
assert!(tauri.contains("error_code = error.code().code()"));
|
||||
assert!(tauri.contains("SOL Prices Desk Tauri command failed"));
|
||||
assert!(!tauri.contains("error_message = error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_remains_manual_without_browser_persistence_or_price_scheduler() {
|
||||
let root = app_root();
|
||||
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
for forbidden in ["setInterval(", "setTimeout(", "requestAnimationFrame(", "localStorage", "sessionStorage", "fetch(", "XMLHttpRequest"] {
|
||||
assert!(!main.contains(forbidden), "pre.007 must remain manual and backend-owned: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
112
deltas/0.2.12/pre.007.md
Normal file
112
deltas/0.2.12/pre.007.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Delta `0.2.12-pre.007` — UX et instrumentation SOL Prices Desk
|
||||
|
||||
## 1. Identité
|
||||
|
||||
```text
|
||||
release : 0.2.12
|
||||
tranche : pre.007
|
||||
version Cargo : 0.2.12-pre.7
|
||||
base : 0.2.12-pre.6
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.006` est intégralement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, `cargo test -p ksp-app-solprices-desk`, `cargo test --workspace` et lancement `cargo tauri dev` passent. Le parcours manuel confirme également le fonctionnement du multi-refresh.
|
||||
|
||||
## 2. Scope réalisé
|
||||
|
||||
`pre.007` finalise l'ergonomie du SOL Prices Desk sans modifier les contrats de dispatch Off-chain :
|
||||
|
||||
```text
|
||||
résumé états providers Providers / Ready / Observed / Attention / Refreshing
|
||||
table responsive + header sticky + scroll horizontal borné
|
||||
colonnes secondaires Semantics/Auth adaptatives
|
||||
availability badges génériques provider-neutral
|
||||
prix String exacte inchangée
|
||||
timestamps rendu humain + millisecondes exactes
|
||||
provider timestamp absent Not supplied, jamais remplacé par received_at
|
||||
sélection row + select all + clear selection
|
||||
refresh row / selected / all inchangés
|
||||
nouvelle commande IPC aucune
|
||||
```
|
||||
|
||||
## 3. Lisibilité et fraîcheur
|
||||
|
||||
La table ne transforme pas la vérité métier :
|
||||
|
||||
- le prix exact reste la `String` canonique fournie par `MarketPriceDecimal` ;
|
||||
- aucun `parseFloat` ni `Number(provider.price)` n'est introduit ;
|
||||
- les millisecondes exactes restent visibles pour les timestamps ;
|
||||
- une présentation locale/ISO est calculée uniquement pour l'affichage temporel, après validation d'une valeur entière compatible `Date` ;
|
||||
- `provider_timestamp = None` est rendu comme `Not supplied` ;
|
||||
- `received_at` reste une ligne KSP distincte ;
|
||||
- `retry_at` est affiché séparément lorsqu'il existe ;
|
||||
- `loading` ne remplace pas l'availability persistée : il devient uniquement le badge visuel temporaire `Refreshing`.
|
||||
|
||||
## 4. Interactions frontend
|
||||
|
||||
La sélection gagne deux contrôles purement locaux :
|
||||
|
||||
```text
|
||||
select all providers
|
||||
clear selection
|
||||
```
|
||||
|
||||
Ils ne déclenchent aucun appel réseau et ne créent aucun état persistant navigateur. Le master checkbox expose les états `checked`/`indeterminate` à partir de la sélection courante. Les boutons batch restent désactivés pendant une opération visible `loading`.
|
||||
|
||||
## 5. Instrumentation
|
||||
|
||||
Le bridge Logging couvre désormais l'ensemble des contrôles :
|
||||
|
||||
```text
|
||||
navigation/view activation debug
|
||||
sélection row debug + provider_id opaque + bool
|
||||
select all debug + selectedCount
|
||||
clear selection debug + selectedCount
|
||||
Refresh row debug + provider_id opaque
|
||||
Refresh selected debug + selectedCount
|
||||
Refresh all debug + providerCount
|
||||
IPC request/completion debug / trace
|
||||
rendu table trace + row/observed/attention counts
|
||||
échec frontend warn sans payload métier
|
||||
échec command backend warn + command/error_domain/error_code
|
||||
```
|
||||
|
||||
Aucun prix, timestamp, body provider, URL, header, API key, credential ou DTO complet n'est journalisé.
|
||||
|
||||
## 6. Canaris
|
||||
|
||||
Les canaris `pre.007` vérifient :
|
||||
|
||||
- présence du résumé, du master select, de `Clear selection`, du header sticky et des styles responsive ;
|
||||
- projection des états génériques actuels sans branche provider spécifique ;
|
||||
- absence de coercition flottante du prix ;
|
||||
- distinction explicite entre provider timestamp et KSP receipt timestamp ;
|
||||
- instrumentation de tous les contrôles utilisateur ;
|
||||
- projection backend des erreurs limitée à `command`, `error_domain` et `error_code` ;
|
||||
- absence de `fetch`, `XMLHttpRequest`, `localStorage`, `sessionStorage`, `setInterval`, `setTimeout` et `requestAnimationFrame` dans le frontend prix.
|
||||
|
||||
## 7. Hors scope confirmé
|
||||
|
||||
```text
|
||||
hardening hostile batch/package final pre.008
|
||||
Wallet Desk Off-chain runtime pre.009
|
||||
Wallet Desk price DTO/commands pre.010
|
||||
Wallet Desk price UI pre.011
|
||||
auto-refresh / polling OUT 0.2.12
|
||||
prix canonique / fallback / moyenne OUT 0.2.12
|
||||
valorisation USD Wallet OUT 0.2.12
|
||||
```
|
||||
|
||||
## 8. Gate
|
||||
|
||||
À exécuter par l'opérateur :
|
||||
|
||||
```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/0.2.12
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-solprices-desk
|
||||
cargo test --workspace
|
||||
(cd crates/ksp-app-solprices-desk && cargo tauri dev)
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 13 -->
|
||||
|
||||
# Plan `0.2.12` — SOL Prices Desk + intégration prix Wallet Desk
|
||||
|
||||
@@ -526,15 +526,15 @@ Le gate de `pre.005` confirme que le refresh individuel fonctionne et que le run
|
||||
|
||||
### `pre.006` — Refresh multiple et global
|
||||
|
||||
**Statut : implémenté ; gate opérateur à confirmer.**
|
||||
**Statut : réalisé ; gate opérateur Cargo/workspace et parcours manuel multi-refresh intégralement PASS.**
|
||||
|
||||
Ajout de `refresh_market_prices` et `refresh_all_market_prices` sur les APIs génériques `MarketPriceService::refresh_many`/`refresh_all`. La sélection frontend est reconstruite dans l'ordre courant des rows, le backend valide atomiquement doublons/IDs inconnus/conflits `in_flight`, marque toutes les lignes concernées `loading`, relâche le mutex de présentation pendant l'`await`, puis applique les outcomes dans leur ordre déterministe. `MarketPriceRefreshManyRequestDto` transporte uniquement les IDs opaques et `MarketPriceRefreshResultDto` retourne les rows affectées avec `requested_count`/`refreshed_count`. Les cooldown/retry deadlines restent les projections Off-chain existantes ; aucun polling, sleep consommateur, fallback automatique ou consensus provider n'est introduit.
|
||||
|
||||
### `pre.007` — UX et instrumentation SOL Prices Desk
|
||||
|
||||
**Statut : planifié.**
|
||||
**Statut : implémenté ; gate opérateur à confirmer.**
|
||||
|
||||
Finalisation de la table responsive, interactions utilisateur, états visuels, instrumentation frontend/backend et polish du Desk. La tranche reste manuelle : aucun auto-refresh ni persistance navigateur n'est ajouté.
|
||||
Finalisation de la table responsive sans changer le contrat de dispatch : résumé providers/ready/observed/attention/refreshing, header sticky, colonnes secondaires adaptatives, badges génériques d'auth/semantics/availability, prix exact conservé en String et timestamps présentés à la fois sous forme humaine et en millisecondes exactes. `provider_timestamp = None` est rendu explicitement comme `Not supplied`, sans substitution par `received_at`. La sélection gagne un contrôle global et `Clear selection`, uniquement frontend. Chaque nouveau contrôle est journalisé en `debug`; les rendus et comptes sûrs restent en `trace`. Les wrappers Tauri journalisent les échecs avec seulement `command`, `error_domain` et `error_code`. Aucun prix, timestamp, payload provider, URL, header ou credential n'entre dans les logs. La tranche reste manuelle : aucune nouvelle commande métier, aucun auto-refresh, scheduler ou persistance navigateur n'est ajouté.
|
||||
|
||||
### `pre.008` — Hardening SOL Prices Desk
|
||||
|
||||
@@ -590,7 +590,7 @@ Préparation du prompt de la release suivante, de `CHANGELOG.md` et de `ROADMAP.
|
||||
|
||||
Mécanique de publication stable uniquement, après validation de la dernière prerelease et vérification de la cohérence de release. Aucun défaut fonctionnel ou documentaire ne doit être absorbé par `rel.001`.
|
||||
|
||||
Le point de packaging transversal de `pre.003` est désormais fermé. Le prochain risque fonctionnel est borné aux refresh `pre.005`/`pre.006`, sans remettre en cause le découpage souple ni les responsabilités documentaires de clôture.
|
||||
Le packaging transversal et les refresh manuels du Price Desk sont désormais fermés par les gates opérateur jusqu’à `pre.006`. Après le polish `pre.007`, le prochain risque fonctionnel est borné au hardening `pre.008` avant l’ouverture du couloir Wallet Desk `pre.009+`, sans remettre en cause le découpage souple ni les responsabilités documentaires de clôture.
|
||||
|
||||
## 20. Critères de sortie `pre.001`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/015-V0_2_12_SOL_PRICES_DESK.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Validation `0.2.12` — SOL Prices Desk + intégration Wallet Desk
|
||||
|
||||
@@ -51,11 +51,12 @@ NOT RUN commande non exécutée dans le contexte indiqué
|
||||
| splash Price Desk | lifecycle KSP commun | PASS |
|
||||
| main shell Price Desk | header/navigation KSP | PASS |
|
||||
| table providers | registry provider-neutral, ordre stable | PASS |
|
||||
| table responsive UX | sticky/badges/timestamps distincts | PASS |
|
||||
| prix exact | String issue de `MarketPriceDecimal` | PASS |
|
||||
| availability/fraîcheur | état générique + timestamps réels | PASS |
|
||||
| refresh row | `MarketPriceService::refresh` | PASS |
|
||||
| refresh selected/many | `MarketPriceService::refresh_many` | PENDING |
|
||||
| refresh all | `MarketPriceService::refresh_all` | PENDING |
|
||||
| refresh selected/many | `MarketPriceService::refresh_many` | PASS |
|
||||
| refresh all | `MarketPriceService::refresh_all` | PASS |
|
||||
| Diagnostics | profils/counts sûrs seulement | PASS |
|
||||
| Wallet price panel | Dashboard compact provider-neutral | PENDING |
|
||||
| auto-refresh | aucun scheduler/polling | OUT |
|
||||
@@ -63,17 +64,17 @@ NOT RUN commande non exécutée dans le contexte indiqué
|
||||
|
||||
## 4. DTO/command matrix
|
||||
|
||||
| Contrat | Entrée/sortie sûre | Statut |
|
||||
|----------------------------------------|--------------------------------------------------------------------|---------|
|
||||
| `MarketPriceRuntimeStatusDto` | profiles/counts sans secret | PASS |
|
||||
| `MarketPriceProviderRowDto` | ID/display/pair/semantics/auth/availability/price/timestamps/retry | PASS |
|
||||
| single refresh direct | provider_id opaque seulement | PASS |
|
||||
| `MarketPriceRefreshManyRequestDto` | liste bornée d’IDs opaques | PENDING |
|
||||
| `MarketPriceRefreshResultDto` | rows/outcome classifiés | PENDING |
|
||||
| runtime/list/refresh/many/all commands | aucun URL/header/API key/body | PENDING |
|
||||
| TS-RS exports | bindings générés non versionnés | PASS |
|
||||
| provider-specific type dans IPC | interdit | OUT |
|
||||
| prix `f64` comme vérité IPC | interdit | OUT |
|
||||
| Contrat | Entrée/sortie sûre | Statut |
|
||||
|----------------------------------------|--------------------------------------------------------------------|--------|
|
||||
| `MarketPriceRuntimeStatusDto` | profiles/counts sans secret | PASS |
|
||||
| `MarketPriceProviderRowDto` | ID/display/pair/semantics/auth/availability/price/timestamps/retry | PASS |
|
||||
| single refresh direct | provider_id opaque seulement | PASS |
|
||||
| `MarketPriceRefreshManyRequestDto` | liste d’IDs opaques ; borne hostile à durcir en `pre.008` | PASS |
|
||||
| `MarketPriceRefreshResultDto` | rows/outcome classifiés | PASS |
|
||||
| runtime/list/refresh/many/all commands | aucun URL/header/API key/body | PASS |
|
||||
| TS-RS exports | bindings générés non versionnés | PASS |
|
||||
| provider-specific type dans IPC | interdit | OUT |
|
||||
| prix `f64` comme vérité IPC | interdit | OUT |
|
||||
|
||||
## 5. Config/composite matrix
|
||||
|
||||
@@ -94,35 +95,36 @@ NOT RUN commande non exécutée dans le contexte indiqué
|
||||
|
||||
## 6. Provider-agnostic boundary matrix
|
||||
|
||||
| Canari | Attendu | Statut |
|
||||
|--------------------------------------------|------------------|---------|
|
||||
| app dépend du service/registry générique | oui | PASS |
|
||||
| branche `coingecko`/`kraken`/etc. dans app | aucune | PASS |
|
||||
| SDK provider dans app | aucun | PASS |
|
||||
| reqwest/provider fetch frontend | aucun | PASS |
|
||||
| endpoint/header/API key dans DTO | aucun | PASS |
|
||||
| rate limiter/sleep/retry provider dans app | aucun | PASS |
|
||||
| refresh many ordre service | préservé | PENDING |
|
||||
| semantics provider-neutral visibles | oui, sans fusion | PASS |
|
||||
| prix canonique/fallback/moyenne | aucun | OUT |
|
||||
| Canari | Attendu | Statut |
|
||||
|--------------------------------------------|------------------|--------|
|
||||
| app dépend du service/registry générique | oui | PASS |
|
||||
| branche `coingecko`/`kraken`/etc. dans app | aucune | PASS |
|
||||
| SDK provider dans app | aucun | PASS |
|
||||
| reqwest/provider fetch frontend | aucun | PASS |
|
||||
| endpoint/header/API key dans DTO | aucun | PASS |
|
||||
| rate limiter/sleep/retry provider dans app | aucun | PASS |
|
||||
| refresh many ordre service | préservé | PASS |
|
||||
| semantics provider-neutral visibles | oui, sans fusion | PASS |
|
||||
| prix canonique/fallback/moyenne | aucun | OUT |
|
||||
|
||||
## 7. Security matrix
|
||||
|
||||
| Risque/canari | Attendu | Statut |
|
||||
|--------------------------------|----------------------------------------------|---------|
|
||||
| URL arbitraire depuis frontend | impossible par contrat command | PASS |
|
||||
| extraction API key/header | aucune projection/log | PASS |
|
||||
| provider_id inconnu | erreur KSP sûre | PASS |
|
||||
| batch duplicate/oversized | rejet avant dispatch | PENDING |
|
||||
| refresh spam | limites/cooldown Off-chain toujours actifs | PASS |
|
||||
| stale observation | timestamps/état distingués | PASS |
|
||||
| double-click race | in-flight/order contrôlés | PENDING |
|
||||
| close pendant refresh | aucune persistance/sûreté frontend requise | PENDING |
|
||||
| CWD/resource confusion | canaris propres à chaque app | PENDING |
|
||||
| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS |
|
||||
| plugin dialog Price Desk | absent | PASS |
|
||||
| frontend sensitive logging | action/ID/count/code seulement | PASS |
|
||||
| Wallet VIEW/OWNER/secrets | non-régression | PENDING |
|
||||
| Risque/canari | Attendu | Statut |
|
||||
|--------------------------------|-------------------------------------------------|---------|
|
||||
| URL arbitraire depuis frontend | impossible par contrat command | PASS |
|
||||
| extraction API key/header | aucune projection/log | PASS |
|
||||
| provider_id inconnu | erreur KSP sûre | PASS |
|
||||
| batch duplicate | rejet avant dispatch | PASS |
|
||||
| batch oversized | borne hostile applicative à durcir en `pre.008` | PENDING |
|
||||
| refresh spam | limites/cooldown Off-chain toujours actifs | PASS |
|
||||
| stale observation | timestamps/état distingués | PASS |
|
||||
| double-click race | in-flight/order contrôlés | PASS |
|
||||
| close pendant refresh | aucune persistance/sûreté frontend requise | PENDING |
|
||||
| CWD/resource confusion | canaris propres à chaque app | PENDING |
|
||||
| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS |
|
||||
| plugin dialog Price Desk | absent | PASS |
|
||||
| frontend sensitive logging | action/ID/count/code seulement | PASS |
|
||||
| Wallet VIEW/OWNER/secrets | non-régression | PENDING |
|
||||
|
||||
## 8. Wallet integration matrix
|
||||
|
||||
@@ -529,4 +531,37 @@ python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
|
||||
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
|
||||
```
|
||||
|
||||
Les lignes `refresh selected/many`, `refresh all`, `MarketPriceRefreshManyRequestDto`, `MarketPriceRefreshResultDto`, ordre many, batch duplicate/oversized et double-click race de la matrice générale restent `PENDING` jusqu'au gate opérateur de cette tranche.
|
||||
Le gate opérateur de `pre.006` fourni le 2026-08-27 ferme ensuite cette tranche : audits, `cargo check`, Clippy, test ciblé et `cargo test --workspace` passent, `cargo tauri dev` démarre correctement et le parcours manuel confirme que le multi-refresh fonctionne. Les lignes `refresh selected/many`, `refresh all`, les deux DTOs batch, l'ordre many, le rejet des doublons et le conflit `in_flight` passent donc à `PASS`. La borne hostile d'une requête batch artificiellement surdimensionnée reste volontairement `PENDING` pour le hardening `pre.008`.
|
||||
|
||||
## 23. Gate `0.2.12-pre.007`
|
||||
|
||||
La tranche finalise l'UX et l'instrumentation sans modifier le moteur Off-chain ni ajouter de nouvelle commande métier :
|
||||
|
||||
```text
|
||||
résumé Providers / Ready / Observed / Attention / Refreshing implémenté
|
||||
header table sticky + scroll horizontal borné implémenté
|
||||
Semantics/Auth secondaires adaptatifs implémenté
|
||||
availability générique par badge implémenté
|
||||
prix exact String inchangée, aucune coercition float
|
||||
timestamp provider absent Not supplied, jamais remplacé par received_at
|
||||
timestamps présents local/ISO + millisecondes exactes visibles
|
||||
select all / clear selection frontend uniquement
|
||||
clics select-all / clear / row / selected / all / navigation debug
|
||||
rendu table + comptes sûrs trace
|
||||
échec command backend command + error_domain + error_code seulement
|
||||
nouvelle command IPC aucune
|
||||
prix/timestamp/payload/URL/header/API key dans les logs aucun
|
||||
auto-refresh / setInterval / setTimeout / requestAnimationFrame absent
|
||||
localStorage / sessionStorage / fetch / XMLHttpRequest absent
|
||||
```
|
||||
|
||||
Canaris statiques ajoutés : table responsive et états génériques, absence de coercition du prix, instrumentation de tous les contrôles, projection sûre des erreurs backend et maintien du fonctionnement exclusivement manuel.
|
||||
|
||||
Validations sandbox après matérialisation :
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
|
||||
TypeScript syntax transpile main.ts PASS
|
||||
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user