SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
9.5 KB

# Market Atlas — Frontend (apps/web)

Next.js 16.3 (App Router, React 19.2, Tailwind v4, TypeScript strict). Package @market-atlas/web.

# Run

bash
# dev (API must run on :8391 — `pnpm dev:api` at the repo root)
pnpm --filter @market-atlas/web dev        # http://localhost:8390
pnpm --filter @market-atlas/web typecheck
pnpm --filter @market-atlas/web build      # NEXT_DIST_DIR=.next-build to build while dev runs
pnpm --filter @market-atlas/web start      # next start -p 8382 -H 127.0.0.1 (production, behind the API edge)
node apps/web/qa/screens.mjs [BASE_URL]    # Playwright sweep: 30 routes × {390,1440} × {dark,light}: status, console errors, overflow, screenshots

Environment (root .env is loaded by next.config.ts): API_URL (server-side base, default http://127.0.0.1:8391), NEXT_PUBLIC_SITE_URL / MA_SITE_URL (canonical URLs, default https://www.market-atlas.co), optional NEXT_PUBLIC_WS_URL (browser WebSocket URL; derived automatically from API_URL in dev, same-origin /v1/stream in production), NEXT_DIST_DIR.

# Data flow

  • Server components call api<T>(path) / apiEnvelope<T> / apiOptional<T> from src/lib/api.ts against API_URL (no-store). A 404 from the API becomes Next notFound(); other errors bubble to error.tsx. apiOptional returns null for non-critical widgets (health panel, changes, events on the home page).
  • Browser code always calls same-origin /v1/... (src/lib/client-api.ts). In dev next.config.ts rewrites /v1/:path* → API_URL; in production the API process is the public edge (:8380): it serves /v1/* itself and proxies every other path to Next on 127.0.0.1:8382.
  • Live stream — src/lib/stream.ts: one WebSocket per tab (MarketStreamClient singleton), reference-counted channel subscriptions (subscribe / unsubscribe messages), reconnect with jittered backoff, batches parsed into a quote store keyed by instrument id plus an events ring buffer and a tape ring buffer. Listeners are notified at most every 100 ms and only for the keys that changed, so a table never re-renders per tick. Hooks: useMarketStream(channels) (keeps channels subscribed while mounted; returns connection state), useLiveQuote(id) (useSyncExternalStore per instrument), useLiveEvents(max), useTape(max), useNow(ms).
  • Components merge SSR quotes with the stream: a streamed value is used only when its received timestamp is newer than the server quote's updated_at (LivePrice, InstrumentHeader, InstrumentStats, InstrumentTable cells). Price cells flash subtly on change (.flash-up/.flash-down, 600 ms, disabled under reduced motion).

# Routes

Route Data Notes
/ /v1/stats, /v1/markets, /v1/events, /v1/data-health, /v1/changes?window=1h hero + telemetry strip (polls stats every 5 s, animated counters), pulse rows, live tape (tape), live events (events:*), world map, movers, source health, what changed
/live /v1/events?limit=100 + stream events:*, tape filters: asset class, country, event types, severity, min confidence, include quotes; pause/resume freezes the list
/markets, /stocks, /etfs, /indices, /crypto, /forex, /rates, /commodities /v1/markets, /v1/instruments?asset_class=…&quoted=1&sort=… ClassPage (server) → InstrumentTable (client, sortable, live via quotes:class:<CLASS>); sort/page/q/all in search params
/instruments/[id] (+ /stocks/[symbol], /crypto/[symbol], /etfs/…, /indices/…, /forex/… redirects) /v1/instruments/:id, /v1/history/:id, /v1/quotes/:id/provenance, /v1/events?instrument=, /v1/filings?cik= sticky live header, lightweight-charts (1D/5D intraday from consensus bars + live updates; 1M…MAX daily), key stats, Why this price? = Source Coverage Matrix (refreshes every 5 s: observation type, Δ vs consensus in bp, role votes/confirms/validates/excluded with readable exclusion reasons, comparability class, shared-upstream note), CoverageBadge "N / T independent sources · tier · status" in the sticky header, section badge and "Observed by" card (/v1/coverage/:id), events, filings, venue, company, aliases, related; JSON-LD FinancialProduct
/exchanges, /exchanges/[id] /v1/exchanges, /v1/exchanges/:id map + table; detail: state, next transition, sessions, holidays, breadth, gainers/losers/most active, events
/countries, /countries/[code] /v1/countries, /v1/countries/:code atlas by region; detail: venues, indices, rates, FX pairs, equities, events
/events, /events/[id], /halts, /filings /v1/events, /v1/events/:id, /v1/filings server-filtered tables (type, severity, instrument, country, before-cursor); halts page streams events:type:TRADING_HALT
/sources, /connectors, /data-health, /status /v1/sources, /v1/connectors, /v1/data-health, /v1/status provenance directory (rights + real-time badges, role contributor/validator, "likely shares an upstream with …", no endpoints), operations table, health with 48 h sparklines and incidents, public status
/coverage (`?tier=A B C
/search?q= + ⌘K palette /v1/search grouped results; palette debounced 140 ms, keyboard navigation
/compare?ids=&resolution= /v1/compare rebased lines (lightweight-charts), stats table, correlation matrix, instrument picker via search
/methodology, /developers, /licensing static (+ /v1/sources for attributions) copy consistent with apps/api/src/core/consensus.ts
/admin /v1/admin/* with x-ma-admin-token from localStorage["ma-admin-token"] overview, connectors (pause/resume/restart/test, detail with health history/fingerprints/schema-change ack/recent observations/state), schema changes, divergence, storage, discovery probe
robots.ts, sitemap.ts (segmented: static, exchanges, countries, per asset class), manifest.ts, icon.svg, not-found.tsx, error.tsx, segment loading.tsx (markets/events/filings) root loading.tsx deliberately absent so notFound() returns a real 404 status

# Components

  • components/layout: SiteHeader (desktop nav, ⌘K button, stream indicator, theme toggle, market clock strip NY/London/Frankfurt/Tokyo/HK/Sydney with session dots), MobileTabBar (Markets · Live · Search · Events · World), SiteFooter (disclaimer, attributions), SearchProvider/SearchDialog, ThemeToggle (persists ma-theme; THEME_SCRIPT applies it before paint), LogoMark/Wordmark.
  • components/ui: StatusBadge (data status / connector state / market state / severity tones), RightsBadge, LivePrice/ChangeCell, FreshnessLabel ("Updated 320 ms ago" · "Delayed 15 min · 12s ago" · "At close · Fri 16:00 EDT" · "End of day · Sep 11" · "Stale · last update 4h ago" · "Withheld"), RelativeTime, ConfidenceMeter, DataTable (client, sortable, sticky header, responsive hide), Sparkline, Page/PageHeader/Section/Stat/Kv/Pill/Empty.
  • components/market: TelemetryStrip, PulseRow, LiveTape, LiveEvents, EventRow, LiveFeed, InstrumentTable, ClassPage, InstrumentHeader, InstrumentStats, PriceChart, ProvenancePanel (coverage matrix), CoverageBadge, RedundancyPanel (homepage Source Redundancy block from /v1/stats.coverage), CompareView, WorldMap (d3-geo Natural Earth + world-atlas countries-110m, loaded lazily), HealthPanel, ChangesPanel.
  • components/admin/AdminConsole.

# Conventions

  • Design tokens live in globals.css (--canvas/--surface/--ink/--rule/--accent/--positive/--negative/--warning/--stale, light + dark on html[data-theme]); Tailwind utilities map through @theme inline (bg-surface, text-ink-2, border-rule…). Numbers use .mono/.tnum. Dense tables use .table-dense.
  • Formatting (src/lib/format.ts): price precision by magnitude and asset class (FX 4–5 decimals, yields x.xx%, crypto sub-cent), compact volumes, signed percents with −, toMs() parses ISO / epoch / Postgres text timestamps (2026-09-11 19:50:00-04).
  • Never present stale as live: data_status drives every badge and freshness label; withheld quotes render provenance but no value.
  • Responsive grids must declare base columns (grid grid-cols-1 [&>*]:min-w-0 lg:grid-cols-…) — implicit auto columns caused horizontal overflow at 390 px. QA script asserts scrollWidth ≤ clientWidth.
  • Server components never import stream.ts; client components never import lib/api.ts (server-only).

# Source Mesh v2 (2026-09-13)

  • lib/coverage.ts: labels for observation types (TRADE, QUOTE, OFFICIAL_FIX, EOD_CLOSE, STABLECOIN_PROXY…), comparability classes (LIVE / FIX / EOD), roleOf() (votes · confirms · validates · excluded), readable reasonLabel() (stale, not comparable, temporal mismatch, validation only, validator disagrees, outlier), tier labels/targets, formatDeltaBps / deltaTone.
  • ConfidenceMeter accepts proxies / validators and shows a small +N marker when a quote is confirmed by proxies or validators (instrument tables, header).
  • Validator contributions arrive with value: null (withheld) and are rendered as "— validator" with agrees/disagrees from delta_bps; they are never shown as values.
  • Homepage: RedundancyPanel (prominent) + mesh explainer; footer/sitemap/header include /coverage.