fix: Charts 15m/1m/5m stop in the past — GET /candles returns oldest N; selected interval chip is low-contrast #705
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#705
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
On
/chartsand/trade, switching the Price (USD) chart from 1h to 15m (and likewise 5m / 1m) plots candles that stop days in the past, with empty time-scale space to the right of the last bar. A retail report on 2026-08-29 showed the last 15m bar at 21 Aug 2026 11:15 while 1h still reached “now.” The volume pane read 0 on that last bar. This is not browser-specific (reproduced on desktop Firefox, Brave, and private windows, with and without VPN).Same control: the selected interval chip (
15mwhen pressed) is low-contrast on dark chrome — a 14% blue wash that does not read as selected.This is one product bug with two coupled causes plus a related chip contrast fix. Bundle them: the empty-right screenshot is the oldest-N payload sitting inside a 1h time-scale that was not refit on interval switch.
Related: #568 (idle mark-to-market densifies 15m/1m bars), #148 (interval switch must not unmount the canvas), #336 (
fitContentforbidden on live 30s refetch), #226 (stalegetCandlesraces).Environment (production, 2026-08-29)
https://dex.cl8y.com)/chartsand/tradeCurrent codebase
GET
/candlesreturns the oldestlimitrows in the windowindexer/src/api/pairs.rsget_pair_candles:from= now −DEFAULT_CANDLE_LOOKBACK_DAYS(90),to= now (docs/indexer-invariants.mdrow “Candle default time window”).limitdefaults to 200, clamped 1–1000.candles::get_candles:Oldest-first + LIMIT is the opposite of a retail chart. After #568, idle marks write the current bucket for every interval, so 15m / 5m / 1m series are dense (
trade_count = 0, zero volume on mark-only bars — matches the report’s Volume 0).Coverage of default
limit=200:90 days of dense 15m is 8,640 bars — far above the 200 default and the 1000 cap. Raising the client limit alone cannot cover 90d of 1m/15m. The default read must be newest N, chronological on the wire.
The dApp never passes
from/to/ a higherlimit:PriceChart.tsxqueryFn: () => getCandles(pairAddress, interval)andtradePairPrefetch.tsuse that default.Client mapping
sortedValidUsdCandlessorts ASC after fetch — it cannot invent bars the API omitted.Interval switch keeps the 1h time-scale (#336)
PriceChartLightweightCanvas.tsx:createChartruns once per pair mount (#148). Interval changes callsyncCandleSeriesData(setDatawhen first bar time changes).timeScale().fitContent()runs only on initial mount and indicator toggle — not on candlesetData(#336 /docs/frontend.md§ Time-scale zoom).So: 1h viewport extends to “now”; 15m payload ends 21 Aug; empty black to the right. Even after newest-N, a leftover 1h range would leave empty left if 15m only covers ~50h. Interval switch must refit (or reset visible range) without breaking live-refetch zoom.
Selected interval chip is 14% blue
PriceChart.tsxinterval buttons usetab-glass+tab-glass-active/tab-glass-inactive. Active fill inindex.css:rgba(68, 138, 255, 0.14)is#448affat 14%. On--bg-0#0d111cthat is a faint oval —aria-pressedis correct (#214) but sighted users cannot tell which interval is on. Same primitive is used for slippage chips, overlay INDICATORS, Pool Manage actions — any stronger active state must stay blue (#488), not gold fill, and must keep:focus-visiblewith--focus-ring.What is already correct (do not regress)
1m|5m|15m|1h|4h|1d|1w→ else 400 (VALID_INTERVALS,security.rs).limitclamp 1–1000 (#431 /api_limit_lower_bound.rs); negative/zero → 1, not 500.trade_count = 0).key={pairAddress}(#148 / #180).fitContent(#336).timemust be non-decreasing).Why the new implementation is needed
fitContentstill leaves a misleading viewport.Constraints / guardrails
ORDER BY open_time DESC LIMIT nthen re-sort ASC (subquery or application). Do not return DESC to the dApp — lightweight-charts requires non-decreasingtime.limitclamp 1–1000. Do not raise the HTTP max to “cover 90d of 1m.” Do not uncap. Optional later: pan-left pagination withto/from— not required for this ticket if newest-200 (or newest-1000) always includes now.from/todefault. Do not shrink lookback to “fix” oldest-N (that re-breaks LocalTerra/QA empty charts).fitContenton 30s refetch (#336). Refit only on interval switch (and existing mount / indicator toggle). User zoom on a given interval must survive live updates.format!of userintervalinto SQL beyond the existing match arm in rebuild. Unknown /javascript:/../→ 400.from/tostill honor the window. Newest-N applies inside that window (integrators paging history must still work).--blue/#448afffamily). No gold fill, no Buy/Sell green/red, nobtn-primaryon interval chips. Light theme must still contrast.:focus-visiblering unchanged. Do not weaken inactive chips into invisibility..tab-glass-activevs chart-only class: if the stronger fill is global, snapshot slippage / Pool Manage / Indicators in dark and light. Prefer a chart-interval modifier if a global bump fights other chips.limitbars, oldest→newest in the JSON array. CG/CMC paths unchanged.Relevant files
indexer/src/db/queries/candles.rsget_candlesORDER BY … ASC LIMIT(change here)indexer/src/api/pairs.rsget_pair_candles,DEFAULT_CANDLE_LOOKBACK_DAYS,CandleQuery.limitindexer/tests/api_pairs.rsindexer/tests/security.rslimit=99999cap; interval injectionindexer/tests/api_limit_lower_bound.rslimitindexer/tests/candle_usd_mark.rsbody[0]order assumptionsdocs/indexer-invariants.mdfrontend-dapp/src/services/indexer/client.tsgetCandlesdefaultlimit = 200frontend-dapp/src/components/charts/PriceChart.tsxgetCandles(pair, interval)frontend-dapp/src/utils/tradePairPrefetch.tsfrontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsxfitContentpolicyfrontend-dapp/src/components/charts/priceChartLightweightSeriesSync.tssetDatavs liveupdatefrontend-dapp/src/components/charts/__tests__/PriceChart.test.tsxaria-pressed/ aria-live (#214)frontend-dapp/src/components/charts/__tests__/PriceChartLightweightCanvas.test.tsxfitContenton background refreshfrontend-dapp/src/index.css.tab-glass-activefrontend-dapp/e2e/price-chart-smoke.spec.tsdocs/frontend.md§ Trade page — price chart invariantsskills/AGENTS_FRONTEND_PRICE_CHART.mdskills/AGENTS_FRONTEND_DESIGN_SYSTEM.mdtab-glass*/ blue activeskills/AGENTS_INDEXER_CANDLE_USD_MARK.mdRecommended direction
Indexer
get_candles: wrap as newest-N then ASC, e.g.Integration test: seed > limit 15m bars spanning several days; default GET last
open_timeis the newest seeded bar, not the oldest; array is still strictly increasingopen_time;limit=200still returns ≤ 200.Optional dApp
limit: keep 200 or usemin(1000, …)for 1m/5m/15m only. Newest-200 of 15m ≈ 50h of recent data — enough for the cutoff report. Do not require 1000 if indexer newest-N is correct.Interval switch refit: when
intervalchanges (detect via first-bar time / query key), callfitContent()once aftersetData. Regression: existing test background data refresh does not call fitContent stays green; new test: intervalsetDatadoes callfitContent.Selected chip: raise active blue (e.g.
--accent-surface/ higher#448affmix + stronger border) on interval buttons until the pressed chip is obvious on dark and light. WCAG-ish contrast vs unselected. Keeparia-pressed. Snapshot or computed-style test so 14% wash cannot return.Docs: one row in
docs/indexer-invariants.md(default GET = latestlimitbars, ASC JSON). Note indocs/frontend.mdthat interval switch refits; live refetch does not.Acceptance criteria
GET /api/v1/pairs/{addr}/candles?interval=15m(nofrom/to) last elementopen_timeis the newest stored 15m bar in the 90d window (same for1m/5m).open_timenon-decreasing). Empty pair →[].limitstill clamp 1–1000;limit=99999→ ≤ 1000;limit=-1/0→ 200 OK, ≤ 1 row.from/tostill filter; newest-N is within that range (oldest-N must not return)./chartsand/trade: 1h → 15m (and 5m, 1m) last candle is current (within one interval of indexer now), not days stale. Empty-right gap from a leftover 1h range is gone (plot fits the new series).trade_count = 0, #568).aria-pressed="true"only on the selected interval; keyboard:focus-visiblering remains.tab-glass-activestill acceptable if the fill is shared; no gold wash, no*-neo.Test plan (all paths)
Indexer
GET .../candles?interval=15m(default limit 200):len==200, firstopen_time> oldest seed, lastopen_time== newest seed, times sorted ASC.1m,5m,1h(1h with 250 hourly bars: last bar is newest).limit=10: newest 10, ASC.limit=1000andlimit=99999: ≤ 1000, newest-capped.from/tocovering only an old slice: returns newest-N inside that slice (not leaking bars afterto).from/toinverted or unparseable: existing behavior (ignore / default window) — do not 500.3h,javascript:alert(1),1m;drop: 400.trade_count=0.intervalomitted: default1h, still newest-N.Frontend unit / charts Vitest
getCandlescall includesinterval; do not send DESC-ordered data into the canvas without client sort (mapper already sorts — keep).PriceChart.test.tsx: 1h → 15m still updates aria-live +aria-pressed(#214); mock 15m payload newer than 1h first bar.setDatacallsfitContent; background refetch (same first time, appended/last-barupdate) does not.createChartcount increases; interval spam does not.tab-glass-active(or new modifier) present on selected interval; contrast/class regression if 14% gradient is the old selector.data-theme='light').1/xon USD.E2E / QA (columbus-5 or LocalTerra with dense 15m)
/charts: 1h → 15m → 5m → 1m → 1h. Each time, last candle time is within one interval of “now” (or last indexed swap/mark). No multi-day empty-right./tradesame toolbar.update.Test plan — attack, hack, and abuse
interval=1h%20OR%201=1, quotes,;,../,javascript:→ 400, no 500, no extra rows (security.rspattern).limit=999999999,limit=-1, overflow strings → clamp, never unboundedLIMIT, never 500 from negative LIMIT (#431).from/tospanning years withlimit=1000→ at most 1000 rows (newest inside window). No full-table scan without LIMIT.open_timeon the mapped series (or indexer contract test on ASC).addrof pair A must not return pair B candles after the query rewrite (JOIN/WHERE stillpair_id).intervalorlimit, 1h could be served as 15m — query string must remain part of the cache key; dApp React Query key already['candles', pairAddress, interval].open_time: malformed RFC3339 already dropped client-side; API parse failure must not 500.open/closestrings stay numeric; interval never interpolated into HTML. Chip labels are fixedINTERVALSconst, not API text.btn-primaryConfirm/Swap. Interval istab-glass, not a money CTA.stylefrom query params for chip color.Verification criteria
make verify-issue-NNN(new script) runs: indexer candle newest-N test +PriceChartinterval tests + canvas fitContent split +make verify-issue-568(marks still on GET) + frontend lint for*-neo.Mode
Severity
mentioned in merge request !1197
mentioned in commit
54c7c82604mentioned in commit
8491aad6c9mentioned in commit
56e4c4f513mentioned in issue #712
mentioned in issue #717