Repository navigation
feat(markets): expandable terminal price chart + fix(dev): OpenSky proxy path - #5459
Conversation
…oxy path Adds a Bloomberg-terminal-style price chart to the Markets panel: click a ticker to open a modal charting its intraday series (gridlines, green/red gradient, HI/LAST/LO labels). New pure terminalChart() SVG util + unit test; consumes the existing MarketData.sparkline, no faked OHLC. Also fixes the dev-only OpenSky proxy: rewrite /api/opensky to /states/all so flight tracking works in npm run dev (was hitting the API root -> 404). Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
@Developer1010x is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR delivers two independent changes: an expandable Bloomberg-terminal-style price chart modal for the Markets panel (new
Confidence Score: 3/5Safe to merge after fixing the unescaped The market-panel row template builds an HTML attribute string that interpolates src/components/MarketPanel.ts line 54 — the
|
| Filename | Overview |
|---|---|
| src/components/MarketPanel.ts | Adds clickable rows + delegated event listener for chart modal; stock.display is correctly HTML-escaped in element content but not in the aria-label template-string attribute on line 54 |
| src/components/market-chart-modal.ts | New singleton overlay modal following the StoryModal pattern; uses DOM setAttribute for aria-label (safe) and escapeHtml for content; missing focus movement on open reduces keyboard accessibility |
| src/utils/terminal-chart.ts | Pure SVG-generating utility; correct guard for ≥2 finite points, unique gradient IDs, numeric-only interpolation; missing accessible label on the role="img" SVG element |
| vite.config.ts | One-line fix: dev-proxy rewrite for /api/opensky now maps to /states/all instead of an empty path that produced 404s; production relay is unaffected |
| tests/terminal-chart.test.mjs | Good unit tests covering empty-series guard, rising/falling colors, label accuracy, dimensions/formatter, and gradient-ID uniqueness |
Sequence Diagram
sequenceDiagram
participant U as User
participant MP as MarketPanel
participant MCM as market-chart-modal
participant TC as terminalChart()
U->>MP: click / Enter on market row
MP->>MP: resolve _markets[idx]
MP->>MCM: openMarketChartModal(stock)
MCM->>MCM: closeMarketChartModal() (if open)
MCM->>TC: terminalChart(stock.sparkline, opts)
TC-->>MCM: "SVG string (or '' if < 2 points)"
MCM->>MCM: createElement overlay
MCM->>MCM: setTrustedHtml(modal content + SVG)
MCM->>MCM: document.addEventListener keydown(Esc)
MCM->>MCM: document.body.appendChild(overlay)
U->>MCM: Esc / backdrop / close button
MCM->>MCM: closeMarketChartModal()
MCM->>MCM: overlay.remove(), removeEventListener
Reviews (1): Last reviewed commit: "feat(markets): expandable terminal price..." | Re-trigger Greptile
| .map((stock, idx) => { | ||
| const clickable = hasPlottableSeries(stock); | ||
| const attrs = clickable | ||
| ? ` class="market-item market-item-clickable" data-market-chart="${idx}" role="button" tabindex="0" aria-label="${t('components.markets.chart.title', { symbol: stock.display })}"` |
There was a problem hiding this comment.
The
aria-label attribute is built via a template string, but stock.display is interpolated through t() without HTML-escaping first. A ticker whose display value contains a " character (e.g. A"B) would produce aria-label="A"B price chart" — a malformed attribute that closes the attribute early. Every other place in this file that uses stock.display in HTML already wraps it in escapeHtml(); this spot is the odd one out.
| ? ` class="market-item market-item-clickable" data-market-chart="${idx}" role="button" tabindex="0" aria-label="${t('components.markets.chart.title', { symbol: stock.display })}"` | |
| ? ` class="market-item market-item-clickable" data-market-chart="${idx}" role="button" tabindex="0" aria-label="${t('components.markets.chart.title', { symbol: escapeHtml(stock.display) })}"` |
| modalEl.querySelector('.market-chart-close')?.addEventListener('click', closeMarketChartModal); | ||
| document.addEventListener('keydown', escHandler); | ||
|
|
||
| document.body.appendChild(modalEl); |
There was a problem hiding this comment.
Modal missing focus movement on open
After document.body.appendChild(modalEl) the focus stays on the market-panel row that was clicked. A keyboard or screen-reader user pressing Tab will navigate through background content rather than being contained within the dialog. The close button is the natural initial focus target; moving focus there (or to modalEl itself with tabindex="-1") at the end of openMarketChartModal would bring the modal into the accessibility tree's natural reading order and enable the Esc handler to work without the user having to manually re-enter the overlay.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const lastDot = `<circle cx="${x(series.length - 1).toFixed(1)}" cy="${y(last).toFixed(1)}" r="2.5" fill="${color}"/>`; | ||
|
|
||
| return ( | ||
| `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" class="terminal-chart" role="img">` + |
There was a problem hiding this comment.
SVG
role="img" without an accessible name
The SVG is declared as an image (role="img") but carries no aria-label attribute and no <title> child element. Screen readers will announce it as "image" with no further description, giving assistive-technology users no indication of what ticker or price range they are looking at. Adding an aria-label option to TerminalChartOptions (e.g. the ticker symbol + "price chart") and writing it onto the <svg> tag would address this with no breaking change.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…name + dialog focus - MarketPanel: HTML-escape stock.display in the clickable-row aria-label (i18next has escapeValue:false, so t() output was unescaped in an attribute). - terminal-chart: add optional ariaLabel written (attribute-escaped) onto the role=img SVG so screen readers announce the ticker, not just 'image'. - market-chart-modal: pass the ticker label to the chart and move focus to the close button on open so keyboard/SR users are contained in the dialog. - tests: cover ariaLabel rendering + escaping. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
# Conflicts: # AGENTS.md # CONTRIBUTING.md # docs/generated/stats.json
This repository is a fork of koala73/worldmonitor. Of 4,938 commits, none are mine. The README described the project with no fork notice, so the repo read as my own work — which would be misattribution of an AGPL-3.0 project maintained by Elie Habib and ~40 other contributors. Add a banner naming upstream as the author, directing users there to run or star the project, and stating my actual contribution plainly: one merged PR, koala73#5459 (+892/-39 across 37 files, merged 2026-07-25). Also notes that this checkout predates that merge and so does not yet contain it. Claude-Session: https://claude.ai/code/session_012ZLS2PqK7vWnj38jZSxQkD Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
|
Thank you will look fwd to contribute |
Summary
Two independent changes:
npm run dev.1. Terminal price chart (feature)
What: Clicking a market ticker opens a modal with a full terminal-style chart of that ticker's intraday series — horizontal gridlines, a green/red gradient fill keyed to direction, right-axis HI / LAST / LO labels, and a last-price marker. Keyboard accessible (Enter/Space to open, Esc / backdrop to close).
Why it matters: the Markets panel previously only showed a 50×16 sparkline per row; there was no way to see a ticker's movement at a readable size. This adds that without any new data source.
How:
src/utils/terminal-chart.ts—terminalChart(number[], opts) → SVG string, same dependency-free idiom assparkline.ts, so it's trivially unit-testable. It consumes the existingMarketData.sparklineseries; it does not invent OHLC/candlestick data.src/components/market-chart-modal.ts— singleton overlay following the existingStoryModalpattern (setTrustedHtml/ backdrop+Esc close).MarketPanel.ts— stores the rendered rows, marks plottable tickers clickable, opens the modal via one delegated listener.src/styles/main.css— modal + clickable-row styles, using existing theme vars.components.markets.chart.{title,close}added toen.jsonand propagated to all 26 locales vianpm run sync:locales.tests/terminal-chart.test.mjs(wired into thetest:datagate) covering the empty-series guard, direction color, HI/LO/LAST labels, custom formatting, and unique gradient ids.Note: true candlesticks were intentionally not built — no OHLC price-history source exists in the repo, and faking candles from a flat line would be misleading. Candlesticks are a clean follow-up once an OHLC source is added.
2. OpenSky dev-proxy path (dev-only fix)
Issue: in
npm run dev, aircraft/ADS-B tracking fails with[Military Flight Tracking] Failed — upstream may be down;/api/openskyreturns 404.Root cause: production routes
/api/openskythrough the relay (api/opensky.js), which calls OpenSky'sstates/allendpoint. Dev has no relay, sovite.config.tsproxies straight tohttps://opensky-network.org/api— but itsrewritestripped the/api/openskyprefix to an empty string, producing a request to the API root (…/api?lamin=…) instead of…/api/states/all?lamin=…. That path 404s, so every region query returns no flights.Fix: rewrite
/api/opensky→/states/allso the dev proxy targets a real OpenSky endpoint. Production is unaffected (it uses the relay, not this proxy).Verified:
curl "http://localhost:5199/api/opensky?lamin=45&lamax=47&lomin=5&lomax=7"→HTTP 200, 34 live aircraft states (was 404). Anonymous OpenSky is rate-limited (~400 req/day), so heavy local use may eventually 429; that's an upstream quota, not a proxy issue.Duplicates
Verification
npm run typecheck— cleannpx biome lint(changed files) — cleannpm run lint:safe-html— passednpm run sync:locales:check— all 24 locales in syncnpx tsx --test tests/terminal-chart.test.mjs— 7 passed