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 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 09:42:34 -05:00
co-authored by Claude Opus 4.8
parent a792a1ec0c
commit a80be443d1
3 changed files with 42 additions and 2 deletions
+2 -1
View File
@@ -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 <algorithm>
#include <cctype>
@@ -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;
}
+36 -1
View File
@@ -31,6 +31,7 @@
#include <algorithm>
#include <cmath>
#include <ctime>
#include <chrono>
#include <unordered_set>
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::minutes>(
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) ----
+4
View File
@@ -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";