Skip to content

Add 24h/7d sparklines to the crypto grid cards - #18

Merged
ralyodio merged 1 commit into
mainfrom
crypto-sparklines
Aug 8, 2026
Merged

ralyodio merged 1 commit into
mainfrom
crypto-sparklines

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Each grid card now draws an inline SVG price line, with a 24h / 7d toggle, the change across that window beside the session change, and the selection persisted across reloads.

Why a dedicated endpoint

I measured before building on /crypto/bars, and it would not have worked well:

  • Alpaca's multi-symbol bars endpoint paginates and truncates per symbol. One request for two pairs over 7d returned 168 bars for BTC but only 34 for ETH, plus a page token. Twelve pairs is several round trips.
  • Twelve cards do not need ~2,000 OHLCV objects to draw twelve lines a couple of hundred pixels wide.

So /crypto/sparklines?symbols=&period=24h|7d builds and downsamples server-side — 24 points for 24h, 56 for 7d — caches per period, and collapses concurrent misses into one fetch. A whole grid costs one set of upstream requests per minute rather than one per visitor, and 14KB on the wire for all twelve.

Three deliberate details

The downsampler always keeps the first and last point. The line's endpoint sits directly beside the printed price; dropping it would make the card contradict itself.

The line is coloured by its own window, not the session. A pair can be down today inside a rising week — painting the 7d line red because the day was red would misreport it. This one is mutation-checked: colouring by session direction fails the test.

A pair with fewer than two points gets no line, not a flat one. A flat line asserts a stability that was never observed. MATIC/USD is exactly this case in production (the migrated token, no recent hourly bars), so the grid reports "1 without 24h history" rather than leaving a card that looks broken.

Losing the sparklines never costs the prices — both requests are issued together, but the chart one is allowed to fail on its own.

Verification

  • 490 tests pass (19 new), tsc --noEmit clean
  • Verified live in a real DOM against real Alpaca data: 12 cards, 11 lines, MATIC correctly bare, the toggle redrawing BTC from +0.8% to +3.1%, zero page errors

🤖 Generated with Claude Code

Each card now draws an inline SVG price line with a 24h/7d toggle, the change
across that window beside the session change, and the selection persisted.

The series has its own endpoint rather than reusing /crypto/bars, for two
reasons found by measuring rather than assuming:

  - Alpaca's multi-symbol bars endpoint paginates and truncates per symbol. A
    single request for two pairs over 7d returned 168 bars for BTC but only 34
    for ETH plus a page token; twelve pairs is several round trips.
  - Twelve cards do not need ~2,000 OHLCV objects to draw twelve lines a couple
    of hundred pixels wide.

So the series is built and downsampled server-side — 24 points for 24h, 56 for
7d — and cached as a unit per period, with concurrent misses collapsed into one
fetch. A whole grid costs one set of upstream requests per minute rather than
one per visitor, and 14KB on the wire for all twelve.

Three details that are deliberate:

  - The downsampler always keeps the first and last point. The line's endpoint
    sits directly beside the printed price, and dropping it would make the card
    contradict itself.
  - The line is coloured by ITS OWN window, not the session. A pair can be down
    today inside a rising week; painting the 7d line red because the day was red
    would misreport it. Mutation-checked — colouring by session fails the test.
  - A pair with fewer than two points is drawn without a line rather than as a
    flat one, which would assert a stability never observed. MATIC/USD is
    exactly this case in production, so the grid says "1 without 24h history"
    rather than leaving a card that looks broken.

Losing the sparklines never costs the prices: the two requests are issued
together but the chart one is allowed to fail on its own.

490 tests pass (19 new), tsc clean. Verified live in a real DOM against real
Alpaca data: 12 cards, 11 lines, the toggle redrawing +0.8% -> +3.1%, no page
errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

56 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 50

Severity Rule Location
HIGH js-unescaped-html-sink public/auth.js:189
HIGH js-ssrf-outbound-request src/crypto/client.ts:109
HIGH js-ssrf-outbound-request src/crypto/fundamentals.ts:137
HIGH js-ssrf-outbound-request src/providers/alpaca.ts:76
HIGH secret-generic-api-key test/auth.test.ts:190
HIGH secret-generic-credential test/credits.test.ts:24
MEDIUM js-unescaped-html-sink public/app.js:41
MEDIUM js-unescaped-html-sink public/app.js:97
MEDIUM js-unescaped-html-sink public/app.js:232
MEDIUM js-unescaped-html-sink public/app.js:244
MEDIUM js-unescaped-html-sink public/app.js:246
MEDIUM js-unescaped-html-sink public/app.js:338
MEDIUM js-unescaped-html-sink public/app.js:354
MEDIUM js-unescaped-html-sink public/app.js:358
MEDIUM js-unescaped-html-sink public/app.js:362
MEDIUM js-unescaped-html-sink public/app.js:381
MEDIUM js-unescaped-html-sink public/app.js:385
MEDIUM js-unescaped-html-sink public/app.js:389
MEDIUM js-unescaped-html-sink public/app.js:572
MEDIUM js-unescaped-html-sink public/app.js:657
MEDIUM js-unescaped-html-sink public/app.js:678
MEDIUM js-unescaped-html-sink public/app.js:800
MEDIUM js-unescaped-html-sink public/app.js:801
MEDIUM js-unescaped-html-sink public/app.js:810
MEDIUM js-unescaped-html-sink public/app.js:848
MEDIUM js-unescaped-html-sink public/app.js:908
MEDIUM js-unescaped-html-sink public/app.js:913
MEDIUM js-unescaped-html-sink public/app.js:926
MEDIUM js-unescaped-html-sink public/app.js:977
MEDIUM js-unescaped-html-sink public/app.js:1056
MEDIUM js-unescaped-html-sink public/app.js:1062
MEDIUM js-unescaped-html-sink public/app.js:1129
MEDIUM js-unescaped-html-sink public/app.js:1169
MEDIUM js-unescaped-html-sink public/app.js:1218
MEDIUM js-unescaped-html-sink public/app.js:1234
MEDIUM js-unescaped-html-sink public/app.js:1377
MEDIUM js-unescaped-html-sink public/app.js:1379
MEDIUM js-unescaped-html-sink public/app.js:1396
MEDIUM js-unescaped-html-sink public/app.js:1591
MEDIUM js-unescaped-html-sink public/app.js:1603
MEDIUM js-unescaped-html-sink public/app.js:1618
MEDIUM js-unescaped-html-sink public/auth.js:41
MEDIUM js-unescaped-html-sink public/auth.js:59
MEDIUM js-unescaped-html-sink public/auth.js:67
MEDIUM js-unescaped-html-sink public/auth.js:106
MEDIUM js-unescaped-html-sink public/auth.js:193
MEDIUM js-unescaped-html-sink public/auth.js:256
MEDIUM js-unescaped-html-sink public/auth.js:258
MEDIUM js-unescaped-html-sink public/auth.js:275
MEDIUM sql-template-interpolation src/cli.ts:552

…and 6 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit a676c2d into main Aug 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant