From a80be443d157603512be96fbd36874d0f454e5b1 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 18 Oct 2026 09:42:34 -0500 Subject: [PATCH] feat(market): flag a stale price instead of showing it as a fresh green number The aggregate price is deliberately kept on a failed or old refresh (it's never cleared), so the hero number could be hours out of date while still drawn in the "live" green. It now turns amber with an "as of N min/h ago" caption and a hover tooltip carrying the friendly fetch error, when the last refresh failed or the aggregate is >=5 min old (only when the hero is the aggregate, not an exchange pair with its own feed). The hardcoded "Price fetch failed" default is now i18n. Co-Authored-By: Claude Opus 4.8 --- src/services/network_refresh_service.cpp | 3 +- src/ui/windows/market_tab.cpp | 37 +++++++++++++++++++++++- src/util/i18n.cpp | 4 +++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index f8bdbd6..8e1c2da 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -2,6 +2,7 @@ #include "history_scan_policy.h" #include "rpc/rpc_busy_gate.h" #include "../util/logger.h" +#include "../util/i18n.h" // util::tr for the price-fetch-failed default #include #include @@ -1995,7 +1996,7 @@ void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state, { state.market.price_loading = false; state.market.price_error = errorMessage.empty() - ? std::string("Price fetch failed") + ? std::string(util::tr("market_price_fetch_failed")) : errorMessage; } diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 2197913..a62fc67 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace dragonx { @@ -1496,7 +1497,18 @@ static void mktDrawPriceHero(const MktCtx& cx) // ---- HERO PRICE (large, prominent) ---- ImFont* h3 = Type().h3(); std::string priceStr = FormatPrice(heroPrice); - ImU32 priceCol = Success(); + // Stale-price cue: the aggregate price is kept on a failed/old refresh (it's never cleared), so a + // green number can be hours out of date. Flag it amber when the last refresh failed or the + // aggregate is >=5 min old. Only when the hero IS the aggregate (an exchange pair has its own feed). + const bool haveFetch = market.last_fetch_time.time_since_epoch().count() != 0; + const long ageMin = haveFetch + ? (long)std::chrono::duration_cast( + std::chrono::steady_clock::now() - market.last_fetch_time).count() + : 0; + const bool usingAggregate = (heroPrice == market.price_usd); + const bool priceStale = usingAggregate && haveFetch && + (!market.price_error.empty() || ageMin >= 5); + ImU32 priceCol = priceStale ? Warning() : Success(); DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx0, cy), priceCol, priceStr.c_str()); // Ticker label after price @@ -1519,6 +1531,29 @@ static void mktDrawPriceHero(const MktCtx& cx) dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp); dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf); + // Stale "as of Nh ago" caption + hover tooltip, right of the change badge (cursor saved/restored + // so the custom draw-list layout below is undisturbed). + if (priceStale) { + char age[64]; + if (ageMin >= 60) snprintf(age, sizeof(age), TR("market_price_stale_hour"), (int)(ageMin / 60)); + else snprintf(age, sizeof(age), TR("market_price_stale_min"), (int)std::max(1L, ageMin)); + ImVec2 asz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, age); + float ax = bMax.x + Layout::spacingMd(); + float limit = cardMax.x - pad - 260.0f * dp; // reserve the trade pill + chart toggles + if (ax + asz.x < limit) { + dl->AddText(capFont, capFont->LegacySize, ImVec2(ax, bMin.y + badgePadV), Warning(), age); + ImVec2 sv = ImGui::GetCursorScreenPos(); + ImGui::SetCursorScreenPos(ImVec2(ax, bMin.y)); + ImGui::InvisibleButton("##PriceStale", ImVec2(asz.x, bMax.y - bMin.y)); + if (ImGui::IsItemHovered()) { + std::string tip = market.price_error.empty() + ? std::string(TR("market_price_fetch_failed")) : market.price_error; + material::Tooltip(TR("market_price_stale_tip"), tip.c_str()); + } + ImGui::SetCursorScreenPos(sv); + } + } + // (Divider is drawn in the chart block, below the interval buttons.) // ---- TRADE BUTTON (top-right of card) ---- diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index ba1061b..30fb009 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -2222,6 +2222,10 @@ void I18n::loadBuiltinEnglish() strings_["market_24h_change"] = "24h"; strings_["market_price_loading"] = "Loading price data..."; strings_["market_price_unavailable"] = "Price data unavailable"; + strings_["market_price_fetch_failed"] = "Price fetch failed"; + strings_["market_price_stale_min"] = "as of %d min ago"; + strings_["market_price_stale_hour"] = "as of %dh ago"; + strings_["market_price_stale_tip"] = "Showing the last known price. The latest refresh failed: %s"; strings_["market_refresh_price"] = "Refresh price data"; strings_["market_trade_on"] = "Trade on %s";