v0.2.12-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 293
|
# version: 294
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.2.12-pre.6"
|
version = "0.2.12-pre.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
|
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -44,13 +44,16 @@
|
|||||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<section data-view-panel="prices">
|
<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>
|
<div>
|
||||||
<h1 class="h3 mb-1">Prices</h1>
|
<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>
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap app-market-toolbar">
|
||||||
<span id="selectedProviderCount" class="small text-body-secondary">0 selected</span>
|
<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>
|
<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
|
<i class="fa-solid fa-list-check me-1" aria-hidden="true"></i>Refresh selected
|
||||||
</button>
|
</button>
|
||||||
@@ -59,6 +62,40 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 id="pricesLoading" class="app-placeholder">
|
||||||
<div>
|
<div>
|
||||||
<i class="fa-solid fa-spinner fa-spin text-body-secondary mb-3" aria-hidden="true"></i>
|
<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>
|
<p class="text-body-secondary mb-0">Le runtime Off-chain ne contient actuellement aucune ligne provider.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="pricesDiagnostic" class="alert alert-warning mb-3" hidden></div>
|
<div id="pricesDiagnostic" class="alert alert-warning mb-3" role="status" aria-live="polite" hidden></div>
|
||||||
<div id="pricesTableContainer" class="table-responsive" hidden>
|
<div id="pricesTableContainer" class="table-responsive app-market-table-container" hidden>
|
||||||
<table id="marketPriceTable" class="table table-hover align-middle mb-0">
|
<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>
|
<thead>
|
||||||
<tr>
|
<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">Provider</th>
|
||||||
<th scope="col">Pair</th>
|
<th scope="col">Pair</th>
|
||||||
<th scope="col">Semantics</th>
|
<th scope="col" class="d-none d-xl-table-cell">Semantics</th>
|
||||||
<th scope="col">Auth</th>
|
<th scope="col" class="d-none d-xl-table-cell">Auth</th>
|
||||||
<th scope="col">Availability</th>
|
<th scope="col">Availability</th>
|
||||||
<th scope="col">Exact price</th>
|
<th scope="col">Exact price</th>
|
||||||
<th scope="col">Provider timestamp (ms)</th>
|
<th scope="col" class="app-market-time-column">Observation times</th>
|
||||||
<th scope="col">Received at (ms)</th>
|
<th scope="col" class="app-market-time-column">Retry</th>
|
||||||
<th scope="col">Retry at (ms)</th>
|
<th scope="col" class="text-end">Action</th>
|
||||||
<th scope="col">Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="marketPriceRows"></tbody>
|
<tbody id="marketPriceRows"></tbody>
|
||||||
@@ -127,7 +166,7 @@
|
|||||||
<dt class="col-sm-5">Fallback Logging</dt>
|
<dt class="col-sm-5">Fallback Logging</dt>
|
||||||
<dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
<dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
||||||
</dl>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-solprices-desk/frontend/sass/_app.scss
|
// file: crates/ksp-app-solprices-desk/frontend/sass/_app.scss
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
$app-header-height: 72px;
|
$app-header-height: 72px;
|
||||||
$app-footer-height: 42px;
|
$app-footer-height: 42px;
|
||||||
@@ -61,7 +61,10 @@ body {
|
|||||||
max-width: 1320px;
|
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);
|
font-family: var(--bs-font-monospace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,3 +106,130 @@ body {
|
|||||||
.app-placeholder i {
|
.app-placeholder i {
|
||||||
font-size: 2.5rem;
|
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
|
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import ResizeObserver from "resize-observer-polyfill";
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
@@ -17,11 +17,46 @@ installFrontendConsoleBridge("main");
|
|||||||
|
|
||||||
type ViewId = "prices" | "diagnostics";
|
type ViewId = "prices" | "diagnostics";
|
||||||
|
|
||||||
|
type AvailabilityPresentation = {
|
||||||
|
badgeClass: string;
|
||||||
|
iconClass: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
const viewTitles: Record<ViewId, string> = {
|
const viewTitles: Record<ViewId, string> = {
|
||||||
prices: "Prices",
|
prices: "Prices",
|
||||||
diagnostics: "Diagnostics",
|
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[] = [];
|
let currentMarketPriceRows: MarketPriceProviderRowDto[] = [];
|
||||||
const selectedProviderIds = new Set<string>();
|
const selectedProviderIds = new Set<string>();
|
||||||
|
|
||||||
@@ -59,10 +94,14 @@ function bindNavigation(): void {
|
|||||||
frontendTrace("main", "SOL Prices Desk navigation handlers installed");
|
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");
|
const cell = document.createElement("td");
|
||||||
cell.textContent = value;
|
cell.textContent = value;
|
||||||
|
if (className) {
|
||||||
|
cell.className = className;
|
||||||
|
}
|
||||||
row.append(cell);
|
row.append(cell);
|
||||||
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPricesDiagnostic(): void {
|
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 {
|
function synchronizeBatchControls(): void {
|
||||||
const selectedCount = selectedProviderIdsInRowOrder().length;
|
const selectedCount = selectedProviderIdsInRowOrder().length;
|
||||||
|
const providerCount = currentMarketPriceRows.length;
|
||||||
const anyLoading = currentMarketPriceRows.some(provider => provider.loading);
|
const anyLoading = currentMarketPriceRows.some(provider => provider.loading);
|
||||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||||
|
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||||
|
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||||
const selectedCountLabel = document.querySelector<HTMLElement>("#selectedProviderCount");
|
const selectedCountLabel = document.querySelector<HTMLElement>("#selectedProviderCount");
|
||||||
if (selectedButton) {
|
if (selectedButton) {
|
||||||
selectedButton.disabled = selectedCount === 0 || anyLoading;
|
selectedButton.disabled = selectedCount === 0 || anyLoading;
|
||||||
}
|
}
|
||||||
if (allButton) {
|
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) {
|
if (selectedCountLabel) {
|
||||||
selectedCountLabel.textContent = `${selectedCount} selected`;
|
selectedCountLabel.textContent = `${selectedCount} selected`;
|
||||||
}
|
}
|
||||||
|
updateSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function markProvidersLoading(providerIds: string[]): void {
|
function markProvidersLoading(providerIds: string[]): void {
|
||||||
@@ -198,6 +265,7 @@ async function refreshAllMarketPrices(): Promise<void> {
|
|||||||
|
|
||||||
function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
const cell = document.createElement("td");
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "app-market-selection-column";
|
||||||
const checkbox = document.createElement("input");
|
const checkbox = document.createElement("input");
|
||||||
checkbox.type = "checkbox";
|
checkbox.type = "checkbox";
|
||||||
checkbox.className = "form-check-input";
|
checkbox.className = "form-check-input";
|
||||||
@@ -220,13 +288,152 @@ function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProv
|
|||||||
row.append(cell);
|
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 {
|
function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
const cell = document.createElement("td");
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "text-end";
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "btn btn-sm btn-outline-primary text-nowrap";
|
button.className = "btn btn-sm btn-outline-primary text-nowrap";
|
||||||
button.disabled = provider.loading;
|
button.disabled = provider.loading;
|
||||||
button.dataset.providerId = provider.providerId;
|
button.dataset.providerId = provider.providerId;
|
||||||
|
button.setAttribute("aria-label", `Refresh ${provider.displayName}`);
|
||||||
button.innerHTML = provider.loading
|
button.innerHTML = provider.loading
|
||||||
? '<i class="fa-solid fa-spinner fa-spin me-1" aria-hidden="true"></i>Refreshing…'
|
? '<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';
|
: '<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) {
|
for (const provider of rows) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
row.dataset.providerId = provider.providerId;
|
row.dataset.providerId = provider.providerId;
|
||||||
|
row.dataset.availability = provider.availability;
|
||||||
|
row.classList.toggle("app-market-row-loading", provider.loading);
|
||||||
appendSelectionCell(row, provider);
|
appendSelectionCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.displayName);
|
appendProviderCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.pair);
|
appendBadgeCell(row, provider.pair, "text-bg-primary");
|
||||||
appendMarketPriceCell(row, provider.semantics);
|
appendBadgeCell(row, semanticsLabels[provider.semantics] ?? semanticsLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||||
appendMarketPriceCell(row, provider.authMode);
|
appendBadgeCell(row, authLabels[provider.authMode] ?? authLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||||
appendMarketPriceCell(row, provider.loading ? "loading" : provider.availability);
|
appendAvailabilityCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.price ?? "—");
|
appendPriceCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.providerTimestampUnixMillis ?? "—");
|
appendObservationTimeCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.receivedAtUnixMillis ?? "—");
|
appendRetryCell(row, provider);
|
||||||
appendMarketPriceCell(row, provider.retryAtUnixMillis ?? "—");
|
|
||||||
appendRefreshCell(row, provider);
|
appendRefreshCell(row, provider);
|
||||||
tableBody.append(row);
|
tableBody.append(row);
|
||||||
}
|
}
|
||||||
synchronizeBatchControls();
|
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 {
|
function bindMarketPriceControls(): void {
|
||||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||||
|
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||||
|
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||||
selectedButton?.addEventListener("click", () => {
|
selectedButton?.addEventListener("click", () => {
|
||||||
void refreshSelectedMarketPrices();
|
void refreshSelectedMarketPrices();
|
||||||
});
|
});
|
||||||
allButton?.addEventListener("click", () => {
|
allButton?.addEventListener("click", () => {
|
||||||
void refreshAllMarketPrices();
|
void refreshAllMarketPrices();
|
||||||
});
|
});
|
||||||
|
clearButton?.addEventListener("click", () => {
|
||||||
|
clearProviderSelection();
|
||||||
|
});
|
||||||
|
selectAll?.addEventListener("change", () => {
|
||||||
|
selectAllProviders(selectAll.checked);
|
||||||
|
});
|
||||||
synchronizeBatchControls();
|
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 {
|
function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ksp-app-solprices-desk",
|
"name": "ksp-app-solprices-desk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.12-pre.6",
|
"version": "0.2.12-pre.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-solprices-desk/src/app_state.rs
|
// 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.
|
//! Shared backend state owned by the SOL Prices Desk Tauri application.
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ impl crate::AppState {
|
|||||||
fallback_logging_active: runtime.fallback_active,
|
fallback_logging_active: runtime.fallback_active,
|
||||||
provider_count,
|
provider_count,
|
||||||
ready_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(),
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
unavailable_provider_count,
|
unavailable_provider_count,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-solprices-desk/src/tauri.rs
|
// file: crates/ksp-app-solprices-desk/src/tauri.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Tauri runtime assembly for the KSP SOL prices desktop application.
|
//! 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]
|
#[tauri::command]
|
||||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||||
let result = crate::emit_frontend_log_event(payload);
|
let result = crate::emit_frontend_log_event(payload);
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
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();
|
let result = state.runtime_status();
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
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();
|
let result = state.list_market_prices();
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(value) => {
|
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::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;
|
let result = state.refresh_all_market_prices().await;
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
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;
|
let result = state.refresh_market_price(provider_id).await;
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
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;
|
let result = state.refresh_market_prices(request).await;
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
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;
|
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
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",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "KSP SOL Prices Desk",
|
"productName": "KSP SOL Prices Desk",
|
||||||
"version": "0.2.12-pre.6",
|
"version": "0.2.12-pre.7",
|
||||||
"identifier": "com.sasedev.ksp-app-solprices-desk",
|
"identifier": "com.sasedev.ksp-app-solprices-desk",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": {
|
"beforeDevCommand": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
// 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`.
|
//! 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}");
|
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
|
// file: crates/ksp-app-solprices-desk/tests/desktop_security.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Security, ownership and frontend instrumentation canaries for SOL Prices Desk.
|
//! 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}");
|
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 -->
|
<!-- 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
|
# 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
|
### `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.
|
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
|
### `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
|
### `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`.
|
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`
|
## 20. Critères de sortie `pre.001`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/015-V0_2_12_SOL_PRICES_DESK.md -->
|
<!-- 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
|
# 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 |
|
| splash Price Desk | lifecycle KSP commun | PASS |
|
||||||
| main shell Price Desk | header/navigation KSP | PASS |
|
| main shell Price Desk | header/navigation KSP | PASS |
|
||||||
| table providers | registry provider-neutral, ordre stable | PASS |
|
| table providers | registry provider-neutral, ordre stable | PASS |
|
||||||
|
| table responsive UX | sticky/badges/timestamps distincts | PASS |
|
||||||
| prix exact | String issue de `MarketPriceDecimal` | PASS |
|
| prix exact | String issue de `MarketPriceDecimal` | PASS |
|
||||||
| availability/fraîcheur | état générique + timestamps réels | PASS |
|
| availability/fraîcheur | état générique + timestamps réels | PASS |
|
||||||
| refresh row | `MarketPriceService::refresh` | PASS |
|
| refresh row | `MarketPriceService::refresh` | PASS |
|
||||||
| refresh selected/many | `MarketPriceService::refresh_many` | PENDING |
|
| refresh selected/many | `MarketPriceService::refresh_many` | PASS |
|
||||||
| refresh all | `MarketPriceService::refresh_all` | PENDING |
|
| refresh all | `MarketPriceService::refresh_all` | PASS |
|
||||||
| Diagnostics | profils/counts sûrs seulement | PASS |
|
| Diagnostics | profils/counts sûrs seulement | PASS |
|
||||||
| Wallet price panel | Dashboard compact provider-neutral | PENDING |
|
| Wallet price panel | Dashboard compact provider-neutral | PENDING |
|
||||||
| auto-refresh | aucun scheduler/polling | OUT |
|
| 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
|
## 4. DTO/command matrix
|
||||||
|
|
||||||
| Contrat | Entrée/sortie sûre | Statut |
|
| Contrat | Entrée/sortie sûre | Statut |
|
||||||
|----------------------------------------|--------------------------------------------------------------------|---------|
|
|----------------------------------------|--------------------------------------------------------------------|--------|
|
||||||
| `MarketPriceRuntimeStatusDto` | profiles/counts sans secret | PASS |
|
| `MarketPriceRuntimeStatusDto` | profiles/counts sans secret | PASS |
|
||||||
| `MarketPriceProviderRowDto` | ID/display/pair/semantics/auth/availability/price/timestamps/retry | PASS |
|
| `MarketPriceProviderRowDto` | ID/display/pair/semantics/auth/availability/price/timestamps/retry | PASS |
|
||||||
| single refresh direct | provider_id opaque seulement | PASS |
|
| single refresh direct | provider_id opaque seulement | PASS |
|
||||||
| `MarketPriceRefreshManyRequestDto` | liste bornée d’IDs opaques | PENDING |
|
| `MarketPriceRefreshManyRequestDto` | liste d’IDs opaques ; borne hostile à durcir en `pre.008` | PASS |
|
||||||
| `MarketPriceRefreshResultDto` | rows/outcome classifiés | PENDING |
|
| `MarketPriceRefreshResultDto` | rows/outcome classifiés | PASS |
|
||||||
| runtime/list/refresh/many/all commands | aucun URL/header/API key/body | PENDING |
|
| 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 |
|
| TS-RS exports | bindings générés non versionnés | PASS |
|
||||||
| provider-specific type dans IPC | interdit | OUT |
|
| provider-specific type dans IPC | interdit | OUT |
|
||||||
| prix `f64` comme vérité IPC | interdit | OUT |
|
| prix `f64` comme vérité IPC | interdit | OUT |
|
||||||
|
|
||||||
## 5. Config/composite matrix
|
## 5. Config/composite matrix
|
||||||
|
|
||||||
@@ -94,35 +95,36 @@ NOT RUN commande non exécutée dans le contexte indiqué
|
|||||||
|
|
||||||
## 6. Provider-agnostic boundary matrix
|
## 6. Provider-agnostic boundary matrix
|
||||||
|
|
||||||
| Canari | Attendu | Statut |
|
| Canari | Attendu | Statut |
|
||||||
|--------------------------------------------|------------------|---------|
|
|--------------------------------------------|------------------|--------|
|
||||||
| app dépend du service/registry générique | oui | PASS |
|
| app dépend du service/registry générique | oui | PASS |
|
||||||
| branche `coingecko`/`kraken`/etc. dans app | aucune | PASS |
|
| branche `coingecko`/`kraken`/etc. dans app | aucune | PASS |
|
||||||
| SDK provider dans app | aucun | PASS |
|
| SDK provider dans app | aucun | PASS |
|
||||||
| reqwest/provider fetch frontend | aucun | PASS |
|
| reqwest/provider fetch frontend | aucun | PASS |
|
||||||
| endpoint/header/API key dans DTO | aucun | PASS |
|
| endpoint/header/API key dans DTO | aucun | PASS |
|
||||||
| rate limiter/sleep/retry provider dans app | aucun | PASS |
|
| rate limiter/sleep/retry provider dans app | aucun | PASS |
|
||||||
| refresh many ordre service | préservé | PENDING |
|
| refresh many ordre service | préservé | PASS |
|
||||||
| semantics provider-neutral visibles | oui, sans fusion | PASS |
|
| semantics provider-neutral visibles | oui, sans fusion | PASS |
|
||||||
| prix canonique/fallback/moyenne | aucun | OUT |
|
| prix canonique/fallback/moyenne | aucun | OUT |
|
||||||
|
|
||||||
## 7. Security matrix
|
## 7. Security matrix
|
||||||
|
|
||||||
| Risque/canari | Attendu | Statut |
|
| Risque/canari | Attendu | Statut |
|
||||||
|--------------------------------|----------------------------------------------|---------|
|
|--------------------------------|-------------------------------------------------|---------|
|
||||||
| URL arbitraire depuis frontend | impossible par contrat command | PASS |
|
| URL arbitraire depuis frontend | impossible par contrat command | PASS |
|
||||||
| extraction API key/header | aucune projection/log | PASS |
|
| extraction API key/header | aucune projection/log | PASS |
|
||||||
| provider_id inconnu | erreur KSP sûre | PASS |
|
| provider_id inconnu | erreur KSP sûre | PASS |
|
||||||
| batch duplicate/oversized | rejet avant dispatch | PENDING |
|
| batch duplicate | rejet avant dispatch | PASS |
|
||||||
| refresh spam | limites/cooldown Off-chain toujours actifs | PASS |
|
| batch oversized | borne hostile applicative à durcir en `pre.008` | PENDING |
|
||||||
| stale observation | timestamps/état distingués | PASS |
|
| refresh spam | limites/cooldown Off-chain toujours actifs | PASS |
|
||||||
| double-click race | in-flight/order contrôlés | PENDING |
|
| stale observation | timestamps/état distingués | PASS |
|
||||||
| close pendant refresh | aucune persistance/sûreté frontend requise | PENDING |
|
| double-click race | in-flight/order contrôlés | PASS |
|
||||||
| CWD/resource confusion | canaris propres à chaque app | PENDING |
|
| close pendant refresh | aucune persistance/sûreté frontend requise | PENDING |
|
||||||
| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS |
|
| CWD/resource confusion | canaris propres à chaque app | PENDING |
|
||||||
| plugin dialog Price Desk | absent | PASS |
|
| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS |
|
||||||
| frontend sensitive logging | action/ID/count/code seulement | PASS |
|
| plugin dialog Price Desk | absent | PASS |
|
||||||
| Wallet VIEW/OWNER/secrets | non-régression | PENDING |
|
| frontend sensitive logging | action/ID/count/code seulement | PASS |
|
||||||
|
| Wallet VIEW/OWNER/secrets | non-régression | PENDING |
|
||||||
|
|
||||||
## 8. Wallet integration matrix
|
## 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
|
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