5.9 KiB
CLAUDE.md
Guidance for working in this repository.
Overview
market-screener-ui is a SvelteKit 5 single-page application (CSR, no SSR) that serves as the interactive dashboard for the market_screener Fastify API.
- All data comes from the API at
http://localhost:3000(proxied through Vite in dev) - No SSR —
+layout.jsexportsssr = false - Svelte 5 runes syntax (
$state,$derived,$effect,$props)
Commands
npm install # install dependencies (SvelteKit, Vite, Svelte 5)
npm run dev # dev server on port 5173
npm run build # production build → .svelte-kit/output
npm run preview # preview production build
To run the full stack, use npm run dev from the API repo (market_screener/) instead — it starts both servers together using concurrently.
Architecture
No SSR
src/routes/+layout.js exports ssr = false. All data fetching happens in the browser. This avoids Svelte 5 SSR compatibility issues and makes sense for a live-data dashboard.
API Proxy
vite.config.js proxies /api/* → http://localhost:3000 in dev. In production, configure your reverse proxy (nginx/Caddy) to do the same.
Data Loading
- Screener page (
/): data loaded client-side on button click via$lib/api.js - Portfolio page (
/portfolio): data loaded via SvelteKit+page.jsload()function — this fires on navigation and is the correct SvelteKit pattern for CSR page data
Do not use onMount for initial data fetching — use load() in +page.js instead. onMount does not reliably fire in SvelteKit CSR for page-level data.
Project Structure
src/
app.html ← HTML shell
app.css ← Global reset + body styles (no :global() in .svelte files)
routes/
+layout.js ← exports ssr = false
+layout.svelte ← nav bar (Screener / Portfolio links)
+page.svelte ← Screener page
portfolio/
+page.js ← load() function — fetches /api/finance/portfolio
+page.svelte ← Portfolio + SimpleFIN page
lib/
api.js ← All fetch calls to the Fastify API
SignalBadge.svelte ← Signal pill component (Strong Buy / Avoid / etc.)
MarketContext.svelte ← Benchmark strip component
.claude/
launch.json ← Preview server config for Claude Code
vite.config.js ← Vite config with /api proxy
svelte.config.js ← SvelteKit config (adapter-auto)
Key Files
src/lib/api.js
All API calls in one place. If the API base URL changes, change it here only.
screenTickers(tickers) // POST /api/screen
fetchCatalysts() // GET /api/screen/catalysts
fetchPortfolio() // GET /api/finance/portfolio
fetchMarketContext() // GET /api/finance/market-context
src/routes/+page.svelte (Screener)
- Ticker input pre-filled with a default watchlist
screen()calls API and stores results in$stateloadCatalysts()fetches news tickers then immediately callsscreen()— one click, full resultsresultsisnulluntil first screen — nothing renders below the toolbarverdictShort()abbreviates long verdict strings ("🟢 BUY (High Conviction)"→"Strong")
src/routes/portfolio/+page.svelte
- Receives
datafrom+page.jsload function vialet { data } = $props() - Shows
data.errorif load failed,data.advicefor holdings,data.personalFinancefor SimpleFIN section
Svelte 5 Patterns Used
<!-- Reactive state -->
let loading = $state(false);
<!-- Derived values -->
const totalGL = $derived(totalValue - totalCost);
<!-- Derived with block -->
const cards = $derived.by(() => { ... return [...] });
<!-- Props -->
let { ctx } = $props();
let { data } = $props();
<!-- Event handlers (no on:click, use onclick) -->
<button onclick={screen}>Screen</button>
<!-- Conditionals in template -->
{@const mode = getTab(type)}
API Response Shape
The Fastify API serializes asset class instances before sending — asset.getDisplayMetrics() is called server-side and included as asset.displayMetrics. In the browser, use r.asset.displayMetrics directly (not r.asset.getDisplayMetrics() which doesn't exist on plain JSON objects).
// Screener response shape
{
STOCK: [{ asset: { ticker, type, currentPrice, metrics, displayMetrics }, fundamental, inflated, signal }],
ETF: [...],
BOND: [...],
ERROR: [...],
marketContext: { sp500Price, riskFreeRate, vixLevel, rateRegime, volatilityRegime, benchmarks }
}
Styling Conventions
- Dark theme throughout: page background
#0f1117, card sections#0d1117/#111827 - All colors are CSS custom values inline (no CSS variables yet — keep consistent with existing palette)
- Tables:
width: max-content; min-width: 100%inside a.table-wrap { overflow-x: auto }container - First column sticky:
position: sticky; left: 0; background: inherit - Verdict pills:
.verdict-pill.green/yellow/red— colored background tint + text - Monospace font for the ticker input field
white-space: nowrapontbody td— tables scroll horizontally, not wrap
Color palette:
page bg: #0f1117
card bg: #0d1117 / #111827 (header rows)
border: #1e293b
muted: #64748b / #475569
text: #e2e8f0 / #f1f5f9
green: #4ade80 (bg tint: #14532d33)
yellow: #facc15 (bg tint: #71350033)
red: #f87171 (bg tint: #450a0a33)
blue accent: #2563eb / #3b82f6
Conventions
- Do not use
:global()in<style>blocks — put global styles insrc/app.css - Use
load()in+page.jsfor page-level data, notonMount $derivedfor computed values — do not recalculate in templates- Keep
api.jsas the single place for fetch calls - If adding a new page: create
+page.jswith aload()that fetches the needed API endpoint, receive via$props()in the component