fix: /protocol Global stats — keep inline Δ% next to its USD headline #667

Closed
opened 2026-08-26 04:18:45 +00:00 by PlasticDigits · 17 comments
PlasticDigits commented 2026-08-26 04:18:45 +00:00 (Migrated from gitlab.com)

Summary

/protocol Global stats already puts each Δ% in the correct StatBox DOM (GitLab #652), but the value row uses flex justify-between. The headline ($61K, $3.7K) stays left; the chips (+19.74% 24h, +155.5% 24h) pin to the right edge of the cell and sit in the gutter under the next column’s label. Operators read +155.5% 24h as belonging to Last 7d vol, not Last 24h vol.

Same StatBox row is used on Protocol fees. Bundle into one issue: group every inline Δ% with its own headline (Global stats + fees), and stop census tiles from rendering integer counts as 14.00 / 8.000. Do not split “Global stats vs fees” vs “count formatting.”

Related: #652 (inline Δ% + prior-window % — DOM/data shipped, visual grouping leftover), #569 P569-1 / P569-7 (one cell: USD + inline 24h/30d), #586 PFee-2 (fee Δ% is a child, not a sibling card), #653 (keep StatBox variant="flat"), #550 (USD census), #489 (do not add a lecture banner).

Current codebase

#652 merged the data and the parentage. Tests only assert within(tile).getByTestId(...). They do not assert that the chip’s box sits next to the headline. Production (and LocalTerra) therefore still looks like the old “Δ% belongs to the next column” layout.

Layer Behavior today
Global stats UI ProtocolGlobalStats.tsx: shell-panel (protocol-global-stats) + grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4. Liquidity / 24h / 7d / 30d vol pass deltas into flat StatBox. Census tiles (tokens, new tokens/pairs, active pairs, 24h trades) are value-only. UTC-day chart sits below.
Fees UI ProtocolFeeStats.tsx: same flat StatBox with delta / deltaLabel / deltaTestId. grid-cols-2 sm:grid-cols-3. Same visual gutter risk on tablet/desktop.
StatBox value row StatBox.tsx L93: flex items-baseline justify-between gap-2 min-w-0 flex-wrap. Headline <p> is left; Δ% cluster is shrink-0 on the right. justify-between is the visual bug. Testids and title / aria-label stay on the child spans (protocol-stat-liquidity-24h / -30d, protocol-stat-volume-*-chg, protocol-stat-fees-*-chg).
Chrome variant="flat" + .stat-flat / .stat-box-flat (#653). No nested card-glass in the metric grid.
Δ% formatting formatProtocolPct + protocolPctToneFromDisplay: missing / non-finite / XSS-like → em-dash; 0 → 0%; never Infinity. Tone is --color-positive / --color-negative / --ink-dim. Gold is never a fill.
Census formatting formatProtocolCount calls formatNum(n, 4) (significant figures). Integers become 14.00, 8.000, 7.000, 5.000, 151.0. Same panel; same “number does not look like it belongs to its label” class.
Overview JSON Unchanged since #652. liquidity_change_{24h,30d}_pct, volume_change_{24h,7d,30d}_pct, fees_change_{24h,7d,30d}_pct. GET /overview is 60s cache + O(1) rollup.
Tests today ProtocolPage.test.tsx and StatBox.test.tsx check text + DOM parentage only. e2e/protocol-page.spec.ts checks visibility + panel order, not chip vs headline geometry. make verify-issue-652 therefore stays green while the page still misleads.
Docs / skills AGENTS_FRONTEND_PROTOCOL_STATS.md P569-1 / P569-7 / PFee-2 / P652-6 say “same cell” and “inline.” They do not say “Δ% must sit immediately after the headline, not justify-between to the next column.”

Why this is needed

  1. Wrong-column reading is a retail accounting error. A green +155.5% 24h sitting under Last 7d vol is not a polish nits — operators decide “is volume growing?” from that chip. DOM-correct + visually-wrong is still a product bug.
  2. #652 already specified “same cell.” The leftover is layout, not a new feature. Shipping a second “inline Δ%” ticket that only re-parents nodes will not fix the screenshot.
  3. Fees inherit the same flex. A Global-stats-only CSS tweak that leaves StatBox on justify-between will regress the fee row the next time a 24h chip is long (+315.6% 7d).
  4. Census 14.00 / 8.000 looks like a USD or rate. Tokens / pairs / trades are integers. formatNum sig-figs are for compact USD, not counts.

Constraints / guardrails

  • Frontend-only. Do not change overview JSON, rollup SQL, flow_change_pct, snapshot Δ%, or GET /api/v1/protocol/volume/daily.
  • Keep #652 parentage. Δ% nodes stay children of protocol-stat-liquidity / protocol-stat-volume-* / protocol-stat-fees-*. Do not restore sibling cards (protocol-stat-liquidity-24h as its own tile).
  • Keep #653 chrome. StatBox variant="flat" inside shell-panel. No nested card-glass. No new gold fills. Tokens stay #488.
  • Do not change Δ% semantics. Liquidity 24h/30d stay snapshot stock % (P569-2). Volume / fees stay prior-equal-window flow % (P652-1, PFee-2). Missing / null / non-finite → em-dash. Never Infinity / fake 0%.
  • Do not lecture. No new Global stats banner about “Δ% is next to the number.” Trailing vs calendar copy stays #576.
  • Census tiles stay value-only (P652-6). Do not invent Δ% on tokens / pairs / trades.
  • Do not call GET /defillama/daily from /protocol.
  • Do not flip default StatBox to a new layout that breaks Charts / Trader isolated card tiles that have no Δ%. Call sites without delta / deltas must look unchanged.
  • a11y. Keep title + aria-label composition (composeStatAriaLabel). Screen readers already hear the right pairing; do not drop that when fixing the flex.
  • XSS. Continue to render Δ% / labels as text. formatProtocolPct already rejects <script> / javascript:. Do not dangerouslySetInnerHTML. Do not put raw overview strings into style= / className.
  • No indexer / wasm / fee math in this ticket.

Relevant files

File Role
frontend-dapp/src/components/ui/StatBox.tsx Value+Δ% flex; drop justify-between; group chips with the headline
frontend-dapp/src/index.css .stat-flat / .stat-box-flat Optional stack/wrap helper; no second chrome layer
frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx Consumer; keep deltas on liquidity + volume only
frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx Same StatBox; must pick up the grouping fix
frontend-dapp/src/utils/formatProtocolStats.ts formatProtocolCount → integer / locale count, not formatNum sig-figs
frontend-dapp/src/components/ui/__tests__/StatBox.test.tsx Assert grouping class / no justify-between when deltas exist
frontend-dapp/src/pages/ProtocolPage.test.tsx Keep within(tile) parentage; add count integer assertion
frontend-dapp/src/utils/__tests__/formatProtocolStats.test.ts formatProtocolCount(14) === "14" (not 14.00)
frontend-dapp/e2e/protocol-page.spec.ts Geometry: chip box is inside its tile and to the right of (or directly under) its headline, not overlapping the next tile
skills/AGENTS_FRONTEND_PROTOCOL_STATS.md + docs/frontend.md Add a grouping invariant (Δ% immediately after headline; not justify-between)
New scripts/qa/verify-issue-NNN.sh + make verify-issue-NNN Docs grep + Vitest + Playwright geometry (5 workers on e2e-smoke)
  1. StatBox: replace the value-row justify-between with justify-start (or a dedicated .stat-value-row that is flex items-baseline gap-x-2 gap-y-0.5 flex-wrap). Headline first; Δ% cluster immediately after. On wrap, chips go to the next line of the same cell, left-aligned under the headline — never to the cell’s far right.
  2. Liquidity two-chip case: keep +19.74% 24h and — 30d as one cluster (flex gap-2) after $61K, still inside protocol-stat-liquidity.
  3. Narrow cells: if $3.7K + +155.5% 24h cannot fit, wrap the cluster under the USD. Do not shrink the headline below text-sm / do not truncate the %.
  4. Census: formatProtocolCount for finite integers → toLocaleString (or formatNum only when abs >= 1e3 if you truly need K). No trailing .00 on 14 tokens. Non-finite still em-dash.
  5. Docs: one sentence on P569-7 / PFee-2 / new P6xx: “Δ% is visually grouped with its headline (justify-start / wrap), not spaced to the next column.”
  6. Verify script modeled on scripts/qa/verify-issue-652.sh (docs + RTL + e2e-smoke @ 5 workers). Keep verify-issue-652 / 550 / 569 / 586 / 653 green.

Do not invent a second StatBox variant for Protocol only if a single value-row class can serve fees + any future Δ% tile.

Acceptance criteria

  • On md+ 4-column Global stats, each Δ% chip’s bounding box is inside its own protocol-stat-* tile and does not sit under the next column’s label.
  • Liquidity: $ headline + protocol-stat-liquidity-24h + protocol-stat-liquidity-30d read as one group (same cell; chips immediately after or wrapped under the $).
  • Volume 24h / 7d / 30d: protocol-stat-volume-*-chg sits with that tile’s USD, not the neighbor.
  • Protocol fees 24h / 7d / 30d: same grouping for protocol-stat-fees-*-chg.
  • Em-dash Δ% (— 30d) still groups with its headline (dim tone, not a stray gutter glyph).
  • Phone 390 and tablet 820: wrap is OK; chips must not jump into the adjacent tile.
  • Light + dark: numbers stay --ink; +% --color-positive; −% --color-negative; — / 0% --ink-dim.
  • Census: 14 tokens, not 14.00; 8 new tokens, not 8.000. Compact K only if the count is actually ≥ 1000.
  • Testids, overview keys, trailing-window title / aria-label, UTC-day chart, and page order (stats → fees → hub → oracle) unchanged.
  • No nested card-glass in protocol-global-stats / protocol-fee-stats.
  • Old indexer (missing volume_change_*) still renders tiles; Δ% em-dash; chart hidden (P652-6).

Test plan — all paths

Path How
Happy desktop 1280 Fixture with mixed +% / — / 0%. Assert chip getBoundingClientRect(): chip.left >= value.left and chip.right <= tile.right and chip.left < nextTile.left. Repeat for liquidity (2 chips), each volume tile, each fee tile.
Tablet 820 (3-col) Same geometry. 7d vol chip must not sit under 30d vol label.
Phone 390 (2-col) Wrap allowed. Chip still inside its tile; within(tile) parentage unchanged.
Two-chip wrap Long +1234.5% 24h + +12.34% 30d on a narrow liquidity cell: both chips stay in protocol-stat-liquidity.
Single chip + short USD $0 / $3.7K + +155.5% 24h — no justify-between gap.
Em-dash / 0% / missing keys Null Δ% → —; 0 → 0%; old overview without volume_change_* → em-dash; tiles still render.
Loading Skeleton only; no orphan chips.
Error RetryError on overview; no invented 0%.
Census formatProtocolCount(0|8|14|151|1000) unit tests; RTL tokens tile has no .00 for 14.
Fees panel hidden Old indexer without fee fields: Global stats grouping still holds; no fee panel.
UTC chart 7d/30d toggle still under the grid; not a Δ% owner.
Light / dark Visual or computed-style check that grouping class does not restore --ink-subtle on a missing --card-bg.
Charts / Trader StatBox No-Δ% card and flat tiles: label+value only; no layout regression (StatBox.test.tsx + existing Charts/Trader verifies).
RTL parentage Existing within(liq).getByTestId('protocol-stat-liquidity-24h') stays green.
Playwright Extend e2e/protocol-page.spec.ts; --project=e2e-smoke --workers=5.

Test plan — attack, hack, and abuse

This is a display-grouping ticket. Do not treat it as a reason to re-open fee/volume math. Still fail closed on anything that could mis-attribute a % or inject chrome.

Vector Expect
XSS in overview strings volume_change_24h_pct / labels / javascript: / <img onerror> / <script> stay em-dash or text. No innerHTML. No attribute injection into style / className from API strings. Existing formatProtocolPct tests stay.
CSS / class injection Raw % text cannot become a Tailwind class or url(). Tone is only from protocolPctToneFromDisplay on the already-formatted display string.
Wrong-column social engineering Geometry tests are the control: a long +9999% 24h must not visually attach to the next headline. That is the abuse (misleading operators).
Infinity / overflow / 1e309 Still em-dash. Never paint Infinity% in the gutter or as a “next column” chip.
Negative zero / NaN / "" Em-dash. Do not coerce to 0%.
Stale / missing prior window null Δ% → —, not a fabricated green chip that looks like growth on the neighbor tile.
Click / hover target steal Δ% title tooltips must not cover the next tile’s headline (hit-slop). whitespace-nowrap chips must not overflow into the next cell’s click box.
Census spoof token_count as 14.00 / scientific / HTML must not render as HTML. Non-finite → em-dash. Do not parse user query params into counts (?ticker= stay allowlisted; this panel ignores ticker).
DoS No new GET. No live SUM of swap_events. No Llama N+1.
Privilege / wallet /protocol is public census. No wallet prompt, no signing, no localStorage of Δ%.
Open redirect / ticker Unchanged P550-2: unknown / javascript: / ../ → ustc. Grouping CSS must not depend on ?ticker=.

Verification criteria

Done when:

  1. make verify-issue-NNN (new script) is green: docs/skills mention the grouping invariant; StatBox has no justify-between on the value+Δ% row; formatProtocolCount integer tests; Protocol RTL; Playwright geometry on e2e-smoke (5 workers).
  2. make verify-issue-652 550 569 586 653 stay green (skip related recursion as those scripts already do).
  3. Manual /protocol at 390 / 820 / 1280, light + dark: each % reads as belonging to its $ / fee headline. Census tiles show 14 not 14.00.
  4. No overview / indexer / wasm diff required for merge.

Out of scope

  • Changing snapshot vs flow Δ% math, daily chart methodology, or hub/oracle cards.
  • Flattening chrome again (#653).
  • Unique-trader headline (#489 / P550).
  • DeFiLlama range dump (#631 / P652-3).
## Summary `/protocol` **Global stats** already puts each Δ% in the correct `StatBox` DOM (GitLab [#652](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/652)), but the value row uses `flex justify-between`. The headline (`$61K`, `$3.7K`) stays left; the chips (`+19.74% 24h`, `+155.5% 24h`) pin to the **right edge of the cell** and sit in the gutter under the **next** column’s label. Operators read `+155.5% 24h` as belonging to Last 7d vol, not Last 24h vol. Same `StatBox` row is used on **Protocol fees**. Bundle into **one** issue: group every inline Δ% with its own headline (Global stats + fees), and stop census tiles from rendering integer counts as `14.00` / `8.000`. Do **not** split “Global stats vs fees” vs “count formatting.” Related: [#652](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/652) (inline Δ% + prior-window % — DOM/data shipped, visual grouping leftover), [#569](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/569) **P569-1** / **P569-7** (one cell: USD + inline 24h/30d), [#586](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/586) **PFee-2** (fee Δ% is a child, not a sibling card), [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) (keep `StatBox variant="flat"`), [#550](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/550) (USD census), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (do not add a lecture banner). ## Current codebase `#652` merged the data and the parentage. Tests only assert `within(tile).getByTestId(...)`. They do **not** assert that the chip’s box sits next to the headline. Production (and LocalTerra) therefore still looks like the old “Δ% belongs to the next column” layout. | Layer | Behavior today | |-------|----------------| | **Global stats UI** | [`ProtocolGlobalStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx): `shell-panel` (`protocol-global-stats`) + `grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4`. Liquidity / 24h / 7d / 30d vol pass `deltas` into flat `StatBox`. Census tiles (tokens, new tokens/pairs, active pairs, 24h trades) are value-only. UTC-day chart sits below. | | **Fees UI** | [`ProtocolFeeStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx): same flat `StatBox` with `delta` / `deltaLabel` / `deltaTestId`. `grid-cols-2 sm:grid-cols-3`. Same visual gutter risk on tablet/desktop. | | **StatBox value row** | [`StatBox.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/StatBox.tsx) L93: `flex items-baseline justify-between gap-2 min-w-0 flex-wrap`. Headline `<p>` is left; Δ% cluster is `shrink-0` on the right. `justify-between` is the visual bug. Testids and `title` / `aria-label` stay on the child spans (`protocol-stat-liquidity-24h` / `-30d`, `protocol-stat-volume-*-chg`, `protocol-stat-fees-*-chg`). | | **Chrome** | `variant="flat"` + `.stat-flat` / `.stat-box-flat` ([#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653)). No nested `card-glass` in the metric grid. | | **Δ% formatting** | [`formatProtocolPct`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/formatProtocolStats.ts) + `protocolPctToneFromDisplay`: missing / non-finite / XSS-like → em-dash; `0` → `0%`; never `Infinity`. Tone is `--color-positive` / `--color-negative` / `--ink-dim`. Gold is never a fill. | | **Census formatting** | `formatProtocolCount` calls `formatNum(n, 4)` (significant figures). Integers become `14.00`, `8.000`, `7.000`, `5.000`, `151.0`. Same panel; same “number does not look like it belongs to its label” class. | | **Overview JSON** | Unchanged since #652. `liquidity_change_{24h,30d}_pct`, `volume_change_{24h,7d,30d}_pct`, `fees_change_{24h,7d,30d}_pct`. GET `/overview` is 60s cache + O(1) rollup. | | **Tests today** | [`ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) and [`StatBox.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/__tests__/StatBox.test.tsx) check text + DOM parentage only. [`e2e/protocol-page.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/protocol-page.spec.ts) checks visibility + panel order, not chip vs headline geometry. `make verify-issue-652` therefore stays green while the page still misleads. | | **Docs / skills** | [`AGENTS_FRONTEND_PROTOCOL_STATS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) **P569-1** / **P569-7** / **PFee-2** / **P652-6** say “same cell” and “inline.” They do not say “Δ% must sit immediately after the headline, not `justify-between` to the next column.” | ## Why this is needed 1. **Wrong-column reading is a retail accounting error.** A green `+155.5% 24h` sitting under **Last 7d vol** is not a polish nits — operators decide “is volume growing?” from that chip. DOM-correct + visually-wrong is still a product bug. 2. **`#652` already specified “same cell.”** The leftover is layout, not a new feature. Shipping a second “inline Δ%” ticket that only re-parents nodes will not fix the screenshot. 3. **Fees inherit the same flex.** A Global-stats-only CSS tweak that leaves `StatBox` on `justify-between` will regress the fee row the next time a 24h chip is long (`+315.6% 7d`). 4. **Census `14.00` / `8.000` looks like a USD or rate.** Tokens / pairs / trades are integers. `formatNum` sig-figs are for compact USD, not counts. ## Constraints / guardrails - **Frontend-only.** Do **not** change overview JSON, rollup SQL, `flow_change_pct`, snapshot Δ%, or `GET /api/v1/protocol/volume/daily`. - **Keep `#652` parentage.** Δ% nodes stay **children** of `protocol-stat-liquidity` / `protocol-stat-volume-*` / `protocol-stat-fees-*`. Do **not** restore sibling cards (`protocol-stat-liquidity-24h` as its own tile). - **Keep `#653` chrome.** `StatBox variant="flat"` inside `shell-panel`. No nested `card-glass`. No new gold fills. Tokens stay [#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488). - **Do not change Δ% semantics.** Liquidity 24h/30d stay snapshot stock % (**P569-2**). Volume / fees stay prior-equal-window flow % (**P652-1**, **PFee-2**). Missing / `null` / non-finite → em-dash. Never `Infinity` / fake `0%`. - **Do not lecture.** No new Global stats banner about “Δ% is next to the number.” Trailing vs calendar copy stays [#576](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/576). - **Census tiles stay value-only** (**P652-6**). Do not invent Δ% on tokens / pairs / trades. - **Do not call** `GET /defillama/daily` from `/protocol`. - **Do not flip default `StatBox` to a new layout that breaks Charts / Trader** isolated `card` tiles that have no Δ%. Call sites without `delta` / `deltas` must look unchanged. - **a11y.** Keep `title` + `aria-label` composition (`composeStatAriaLabel`). Screen readers already hear the right pairing; do not drop that when fixing the flex. - **XSS.** Continue to render Δ% / labels as **text**. `formatProtocolPct` already rejects `<script>` / `javascript:`. Do not `dangerouslySetInnerHTML`. Do not put raw overview strings into `style=` / `className`. - **No indexer / wasm / fee math** in this ticket. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/components/ui/StatBox.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/StatBox.tsx) | Value+Δ% flex; drop `justify-between`; group chips with the headline | | [`frontend-dapp/src/index.css`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/index.css) `.stat-flat` / `.stat-box-flat` | Optional stack/wrap helper; no second chrome layer | | [`frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx) | Consumer; keep `deltas` on liquidity + volume only | | [`frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx) | Same StatBox; must pick up the grouping fix | | [`frontend-dapp/src/utils/formatProtocolStats.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/formatProtocolStats.ts) | `formatProtocolCount` → integer / locale count, not `formatNum` sig-figs | | [`frontend-dapp/src/components/ui/__tests__/StatBox.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/__tests__/StatBox.test.tsx) | Assert grouping class / no `justify-between` when deltas exist | | [`frontend-dapp/src/pages/ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) | Keep `within(tile)` parentage; add count integer assertion | | [`frontend-dapp/src/utils/__tests__/formatProtocolStats.test.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/__tests__/formatProtocolStats.test.ts) | `formatProtocolCount(14) === "14"` (not `14.00`) | | [`frontend-dapp/e2e/protocol-page.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/protocol-page.spec.ts) | Geometry: chip box is inside its tile and to the right of (or directly under) its headline, not overlapping the next tile | | [`skills/AGENTS_FRONTEND_PROTOCOL_STATS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) + [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) | Add a grouping invariant (Δ% immediately after headline; not `justify-between`) | | New `scripts/qa/verify-issue-NNN.sh` + `make verify-issue-NNN` | Docs grep + Vitest + Playwright geometry (5 workers on e2e-smoke) | ## Recommended direction 1. **StatBox:** replace the value-row `justify-between` with `justify-start` (or a dedicated `.stat-value-row` that is `flex items-baseline gap-x-2 gap-y-0.5 flex-wrap`). Headline first; Δ% cluster immediately after. On wrap, chips go to the **next line of the same cell**, left-aligned under the headline — never to the cell’s far right. 2. **Liquidity two-chip case:** keep `+19.74% 24h` and `— 30d` as one cluster (`flex gap-2`) after `$61K`, still inside `protocol-stat-liquidity`. 3. **Narrow cells:** if `$3.7K` + `+155.5% 24h` cannot fit, wrap the cluster **under** the USD. Do not shrink the headline below `text-sm` / do not truncate the `%`. 4. **Census:** `formatProtocolCount` for finite integers → `toLocaleString` (or `formatNum` only when `abs >= 1e3` if you truly need `K`). No trailing `.00` on 14 tokens. Non-finite still em-dash. 5. **Docs:** one sentence on **P569-7** / **PFee-2** / new **P6xx**: “Δ% is visually grouped with its headline (`justify-start` / wrap), not spaced to the next column.” 6. **Verify script** modeled on `scripts/qa/verify-issue-652.sh` (docs + RTL + e2e-smoke @ 5 workers). Keep `verify-issue-652` / `550` / `569` / `586` / `653` green. Do **not** invent a second `StatBox` variant for Protocol only if a single value-row class can serve fees + any future Δ% tile. ## Acceptance criteria - [ ] On `md+` 4-column Global stats, each Δ% chip’s bounding box is inside its own `protocol-stat-*` tile and does **not** sit under the next column’s label. - [ ] Liquidity: `$` headline + `protocol-stat-liquidity-24h` + `protocol-stat-liquidity-30d` read as one group (same cell; chips immediately after or wrapped under the `$`). - [ ] Volume 24h / 7d / 30d: `protocol-stat-volume-*-chg` sits with that tile’s USD, not the neighbor. - [ ] Protocol fees 24h / 7d / 30d: same grouping for `protocol-stat-fees-*-chg`. - [ ] Em-dash Δ% (`— 30d`) still groups with its headline (dim tone, not a stray gutter glyph). - [ ] Phone 390 and tablet 820: wrap is OK; chips must not jump into the adjacent tile. - [ ] Light + dark: numbers stay `--ink`; +% `--color-positive`; −% `--color-negative`; `—` / `0%` `--ink-dim`. - [ ] Census: `14` tokens, not `14.00`; `8` new tokens, not `8.000`. Compact `K` only if the count is actually ≥ 1000. - [ ] Testids, overview keys, trailing-window `title` / `aria-label`, UTC-day chart, and page order (stats → fees → hub → oracle) unchanged. - [ ] No nested `card-glass` in `protocol-global-stats` / `protocol-fee-stats`. - [ ] Old indexer (missing `volume_change_*`) still renders tiles; Δ% em-dash; chart hidden (**P652-6**). ## Test plan — all paths | Path | How | |------|-----| | **Happy desktop 1280** | Fixture with mixed +% / — / 0%. Assert chip `getBoundingClientRect()`: `chip.left >= value.left` and `chip.right <= tile.right` and `chip.left < nextTile.left`. Repeat for liquidity (2 chips), each volume tile, each fee tile. | | **Tablet 820 (3-col)** | Same geometry. 7d vol chip must not sit under 30d vol label. | | **Phone 390 (2-col)** | Wrap allowed. Chip still inside its tile; `within(tile)` parentage unchanged. | | **Two-chip wrap** | Long `+1234.5% 24h` + `+12.34% 30d` on a narrow liquidity cell: both chips stay in `protocol-stat-liquidity`. | | **Single chip + short USD** | `$0` / `$3.7K` + `+155.5% 24h` — no `justify-between` gap. | | **Em-dash / 0% / missing keys** | Null Δ% → `—`; `0` → `0%`; old overview without `volume_change_*` → em-dash; tiles still render. | | **Loading** | Skeleton only; no orphan chips. | | **Error** | `RetryError` on overview; no invented `0%`. | | **Census** | `formatProtocolCount(0\|8\|14\|151\|1000)` unit tests; RTL tokens tile has no `.00` for 14. | | **Fees panel hidden** | Old indexer without fee fields: Global stats grouping still holds; no fee panel. | | **UTC chart** | 7d/30d toggle still under the grid; not a Δ% owner. | | **Light / dark** | Visual or computed-style check that grouping class does not restore `--ink-subtle` on a missing `--card-bg`. | | **Charts / Trader StatBox** | No-Δ% `card` and `flat` tiles: label+value only; no layout regression (`StatBox.test.tsx` + existing Charts/Trader verifies). | | **RTL parentage** | Existing `within(liq).getByTestId('protocol-stat-liquidity-24h')` stays green. | | **Playwright** | Extend `e2e/protocol-page.spec.ts`; `--project=e2e-smoke --workers=5`. | ## Test plan — attack, hack, and abuse This is a display-grouping ticket. Do **not** treat it as a reason to re-open fee/volume math. Still fail closed on anything that could **mis-attribute** a % or inject chrome. | Vector | Expect | |--------|--------| | **XSS in overview strings** | `volume_change_24h_pct` / labels / `javascript:` / `<img onerror>` / `<script>` stay em-dash or text. No `innerHTML`. No attribute injection into `style` / `className` from API strings. Existing `formatProtocolPct` tests stay. | | **CSS / class injection** | Raw `%` text cannot become a Tailwind class or `url()`. Tone is only from `protocolPctToneFromDisplay` on the **already-formatted** display string. | | **Wrong-column social engineering** | Geometry tests are the control: a long `+9999% 24h` must not visually attach to the next headline. That *is* the abuse (misleading operators). | | **Infinity / overflow / `1e309`** | Still em-dash. Never paint `Infinity%` in the gutter or as a “next column” chip. | | **Negative zero / NaN / `""`** | Em-dash. Do not coerce to `0%`. | | **Stale / missing prior window** | `null` Δ% → `—`, not a fabricated green chip that looks like growth on the neighbor tile. | | **Click / hover target steal** | Δ% `title` tooltips must not cover the next tile’s headline (hit-slop). `whitespace-nowrap` chips must not overflow into the next cell’s click box. | | **Census spoof** | `token_count` as `14.00` / scientific / HTML must not render as HTML. Non-finite → em-dash. Do not parse user query params into counts (`?ticker=` stay allowlisted; this panel ignores ticker). | | **DoS** | No new GET. No live `SUM` of `swap_events`. No Llama N+1. | | **Privilege / wallet** | `/protocol` is public census. No wallet prompt, no signing, no localStorage of Δ%. | | **Open redirect / ticker** | Unchanged **P550-2**: unknown / `javascript:` / `../` → `ustc`. Grouping CSS must not depend on `?ticker=`. | ## Verification criteria Done when: 1. `make verify-issue-NNN` (new script) is green: docs/skills mention the grouping invariant; StatBox has no `justify-between` on the value+Δ% row; `formatProtocolCount` integer tests; Protocol RTL; Playwright geometry on e2e-smoke (5 workers). 2. `make verify-issue-652` `550` `569` `586` `653` stay green (skip related recursion as those scripts already do). 3. Manual `/protocol` at 390 / 820 / 1280, light + dark: each % reads as belonging to **its** `$` / fee headline. Census tiles show `14` not `14.00`. 4. No overview / indexer / wasm diff required for merge. ## Out of scope - Changing snapshot vs flow Δ% math, daily chart methodology, or hub/oracle cards. - Flattening chrome again (#653). - Unique-trader headline (#489 / **P550**). - DeFiLlama range dump (#631 / **P652-3**).
PlasticDigits commented 2026-08-26 04:18:46 +00:00 (Migrated from gitlab.com)

marked as related to #652

marked as related to #652
PlasticDigits commented 2026-08-26 04:18:47 +00:00 (Migrated from gitlab.com)

marked as related to #569

marked as related to #569
PlasticDigits commented 2026-08-26 04:18:48 +00:00 (Migrated from gitlab.com)

marked as related to #586

marked as related to #586
PlasticDigits commented 2026-08-26 04:18:49 +00:00 (Migrated from gitlab.com)

marked as related to #653

marked as related to #653
PlasticDigits commented 2026-08-26 04:18:50 +00:00 (Migrated from gitlab.com)

marked as related to #550

marked as related to #550
PlasticDigits commented 2026-08-26 06:56:56 +00:00 (Migrated from gitlab.com)

mentioned in commit e25bbb04f3

mentioned in commit e25bbb04f3ba1c31c9424db4fed7d69d6109e165
PlasticDigits commented 2026-08-26 06:58:03 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1158

mentioned in merge request !1158
PlasticDigits commented 2026-08-26 06:58:16 +00:00 (Migrated from gitlab.com)

Implementation is in !1158 (fix/667-protocol-stat-delta-group).

StatBox value+Δ% uses .stat-value-row (justify-start / wrap) so chips sit with their own headline. formatProtocolCount renders locale integers (14, not 14.00). make verify-issue-667 is green (RTL + Playwright geometry @ 5 workers); related verify-issue-652 / 653 stayed green.

Leftover: live-indexer visual pass at 390 / 820 / 1280 light+dark on a page with real USD figures (e2e ran with market-data-unavailable em-dashes; grouping still held).

Implementation is in !1158 (`fix/667-protocol-stat-delta-group`). StatBox value+Δ% uses `.stat-value-row` (`justify-start` / wrap) so chips sit with their own headline. `formatProtocolCount` renders locale integers (`14`, not `14.00`). `make verify-issue-667` is green (RTL + Playwright geometry @ 5 workers); related `verify-issue-652` / `653` stayed green. Leftover: live-indexer visual pass at 390 / 820 / 1280 light+dark on a page with real USD figures (e2e ran with market-data-unavailable em-dashes; grouping still held).
PlasticDigits commented 2026-08-26 08:11:27 +00:00 (Migrated from gitlab.com)

mentioned in commit f5ffe9649b

mentioned in commit f5ffe9649ba72d70dd8c45b09a1d8cb24a0f697e
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 08:11:27 +00:00
PlasticDigits commented 2026-08-26 08:15:50 +00:00 (Migrated from gitlab.com)

mentioned in commit 211055203d

mentioned in commit 211055203dea0adedbaeeca0ae8fef542f8e9cec
PlasticDigits commented 2026-08-26 08:19:07 +00:00 (Migrated from gitlab.com)

mentioned in commit c9cf7106b6

mentioned in commit c9cf7106b63dd27f2531fa01c9a29dc46f7c47f0
PlasticDigits commented 2026-08-26 09:30:51 +00:00 (Migrated from gitlab.com)

Merged to main via !1158. Later #652/#668 Protocol chart work was kept; Δ% chips stay grouped with the USD headline.

Leftover: /protocol 375/1280 light+dark after frontend rebuild — Δ% is visually grouped with the matching headline, not a second chrome row.

Merged to `main` via !1158. Later #652/#668 Protocol chart work was kept; Δ% chips stay grouped with the USD headline. Leftover: `/protocol` 375/1280 light+dark after frontend rebuild — Δ% is visually grouped with the matching headline, not a second chrome row.
PlasticDigits commented 2026-08-26 09:30:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #664

mentioned in issue #664
PlasticDigits commented 2026-08-26 09:30:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #668

mentioned in issue #668
PlasticDigits commented 2026-08-26 09:31:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

mentioned in issue #673
PlasticDigits commented 2026-08-27 00:17:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #677

mentioned in issue #677
PlasticDigits commented 2026-08-27 00:17:42 +00:00 (Migrated from gitlab.com)

marked as related to #677

marked as related to #677
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#667
No description provided.