[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CCK7C2CW5EHW08W3041SFB":3,"skill-same:01M6CCK7C2CW5EHW08W3041SFB":115},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":54,"ownerDescription":6,"updatedAt":56,"install":57,"resources":70},"01M6CCK7C2CW5EHW08W3041SFB","interactive-dashboard",null,"Interactive web apps served from the computer via preview URL: dashboards, trackers, screeners, monitors, tools, multi-page or React apps, anything that needs a running server","ginlix-ai\u002FLangAlpha","ginlix-ai","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255505137?v=4","01M6CCJK39A1C27GHF9AP4XWY0","Ginlix AI","01M6CCJK7GR7P1AWNRTPZYDJZY",2297,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002FSKILL.md",[18,23,28],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",{"categoryNo":29,"categoryName":30,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000013","分析与可视化",[32,33,34,35,36,37,38,39,40,41,42,43],"dashboard","interactive dashboard","interactive","portfolio","dashboards","preview url","web","stock","URL","preview","via","Heatmaps","2026-11-10 10:12:30",false,"---\nname: interactive-dashboard\ndescription: \"Interactive web apps served from the computer via preview URL: dashboards, trackers, screeners, monitors, tools, multi-page or React apps, anything that needs a running server\"\n---\n\n# Interactive Web Apps\n\nBuild interactive web apps inside the sandbox and expose them to the user via `GetPreviewUrl`: dashboards, trackers, monitors, screeners, calculators and tools, live visualizations, multi-page apps. Anything the user opens as an app, which needs a server running on the computer.\n\n## When to Use\n\nUse this skill for a **live, served web app** — one that needs a running server, not a single file:\n\n- User asks for a **dashboard**, **tracker**, or **monitor** that **refreshes live data** (polling, auto-update)\n- The app needs **server-side logic** — filtering\u002Fscreening over a large dataset, on-demand fetches, computed endpoints\n- **Multi-page \u002F routed** apps, or anything that needs React-level component interactivity\n- The dataset is **too large to embed** in a single HTML file\n- User explicitly says \"preview\", \"web view\", \"web app\", or wants it running at a URL\n\n**Do NOT use if:**\n- User wants **their LangAlpha Dashboard**, the page they see when they open LangAlpha, built or changed. That's `.agents\u002Fskills\u002Fdashboard-page\u002FSKILL.md`: a page LangAlpha keeps a copy of, which opens while the computer is off. A running app cannot. The user can still set a running app as their Dashboard from the file panel; it opens only while the computer is on.\n- User wants a **self-contained HTML report** — even an *interactive* one (sortable tables, tabs, hover\u002Fzoom charts) over a **data snapshot**. That's `.agents\u002Fskills\u002Fhtml-report\u002FSKILL.md`: one file in the task directory, keepable, printable, PDF-exportable, share-linkable. Interactivity by itself does **not** require a dashboard.\n- User wants a **static chart image** → matplotlib\u002Fplotly `savefig`.\n- User wants an **in-chat figure** → `inline-widget` (`ShowWidget`).\n\n### Dashboard vs. HTML Report\n\nBoth can be interactive, so the divide is **live served app vs. self-contained snapshot file**, not static vs. interactive:\n\n| | interactive-dashboard (this skill) | html-report |\n|---|---|---|\n| Delivery | A **running server**, exposed via `GetPreviewUrl` | One **`.html` file** in `\u003Ctask_name>\u002F` |\n| Data | **Live \u002F refreshing**, fetched from a backend; large datasets OK | A **snapshot** embedded in the file |\n| Interactivity | Full app — routing, server-side filtering, live updates | Client-side over the snapshot — sort, filter, tabs, chart hover\u002Fzoom |\n| Keep \u002F print \u002F share | A URL, live only while the workspace runs | Downloadable, PDF-exportable, share-linkable as one artifact |\n| Pick when | Data must be live, or compute\u002Fscale needs a server | The answer is a deliverable the user keeps |\n\n## Architecture\n\nChoose the tier based on complexity:\n\n| Tier | When | Stack | Serve command |\n|------|------|-------|---------------|\n| **Simple** | Snapshot-at-load data, few charts, no backend logic (still served via preview URL) | Self-contained HTML + CDN libs | `python -m http.server 8050 --bind 0.0.0.0` |\n| **FastAPI + HTML** | Live data refresh, server-side logic, no React needed | FastAPI serves `static\u002F` + `fetch()` polling | `bash start.sh` |\n| **Complex** | Filtering, routing, component interactivity, multi-page | FastAPI backend + Vite\u002FReact frontend | `bash start.sh` |\n\n**Decision rule:** Start with Simple. Escalate to FastAPI + HTML when user needs live data refresh or server-side logic. Escalate to Complex only when user needs React-level component interactivity, client-side routing, or a multi-page SPA.\n\n**Port convention:** Use port **8050** (default). Range 8050-8059 for dashboards.\n\n### CSP \u002F Iframe Safety\n\nThe preview iframe enforces Content Security Policy (CSP). Certain patterns are **silently blocked** — no error banner, just dead UI elements. Always use the safe alternatives:\n\n| Blocked pattern | Safe alternative |\n|-----------------|-----------------|\n| `\u003Cbutton onclick=\"fn()\">` | `el.addEventListener('click', fn)` |\n| `\u003Cdiv onmouseover=\"fn()\">` | `el.addEventListener('mouseover', fn)` |\n| Any `on*=\"...\"` HTML attribute | `el.addEventListener(event, fn)` |\n| `innerHTML` with `onclick` | `document.createElement()` + `addEventListener` |\n| `eval(\"code\")` | Direct function calls |\n| `new Function(\"code\")` | Named function declarations |\n| `setTimeout(\"code string\", ms)` | `setTimeout(fn, ms)` (function reference) |\n| `\u003Ca href=\"javascript:...\">` | `\u003Ca href=\"#\" data-action=\"...\">` + `addEventListener` |\n\n**Quick self-check** — run before serving to catch violations:\n\n```python\nimport subprocess\nresult = subprocess.run(\n    [\"grep\", \"-rnE\", r'on(click|input|change|focus|blur|submit|load|error|mouse|key)\\s*=',\n     \"dashboard\u002F\"],\n    capture_output=True, text=True\n)\nif result.stdout.strip():\n    raise RuntimeError(f\"CSP-unsafe inline handlers found:\\n{result.stdout}\")\n```\n\n**Template literal hygiene** — when building HTML strings in JS template literals, CSS semicolons inside `${}` expressions cause silent parse failures:\n\n```javascript\n\u002F\u002F BAD — semicolon inside ${} terminates the expression early\nconst el = `\u003Cdiv style=\"color:${positive ? 'green' : 'red'; font-weight:600}\">`;\n\n\u002F\u002F GOOD — close the expression first, then continue the attribute string\nconst el = `\u003Cdiv style=\"color:${positive ? 'green' : 'red'};font-weight:600\">`;\n```\n\nRule: **never put a CSS semicolon inside `${}`** — always close `}` before the semicolon.\n\n### How Preview Serving Works\n\n`GetPreviewUrl` is a **platform-level tool** available only to the main agent runtime. It is NOT a Python function — do not `import` it or call it from `execute_code`. The agent invokes it as a tool call.\n\nWhen you call `GetPreviewUrl(port, command, title)`:\n\n1. The **command is persisted to the database** automatically\n2. The platform starts the command in a dedicated sandbox session for that port\n3. It polls until the port is listening, then generates a signed URL\n4. If the port is **already reachable**, the command start is skipped entirely\n\n**Sub-agent fallback:** Sub-agents cannot call `GetPreviewUrl`. Instead, build the dashboard files, start the server for verification, then return the serve details so the orchestrating agent can call `GetPreviewUrl`.\n\n**All tiers:** start the server from Bash with `&` and its output sent to a file, so the call returns at once and the server keeps running:\n\n```bash\n# Simple tier\ncd \u003Ctask> && nohup python -m http.server 8050 --bind 0.0.0.0 > server.log 2>&1 &\n\n# Docker tiers\ncd \u003Ctask> && nohup bash start.sh > server.log 2>&1 &\n```\n\nThen verify it's up in a separate (foreground) Bash call:\n\n```bash\nfor i in $(seq 1 15); do curl -sf http:\u002F\u002F127.0.0.1:8050\u002F > \u002Fdev\u002Fnull && echo \"Server ready\" && exit 0 || sleep 1; done; echo \"FAIL\"; exit 1\n```\n\nThen return **all three fields** to the orchestrating agent (it needs the command for DB persistence \u002F restart recovery):\n\n```\nport: 8050\ncommand: \"cd \u003Ctask> && python -m http.server 8050 --bind 0.0.0.0\"  # or \"bash \u003Ctask>\u002Fstart.sh\"\ntitle: \"AAPL Stock Dashboard\"\n```\n\nThe orchestrating agent calls `GetPreviewUrl(port=8050, command=\"...\", title=\"...\")` which persists the command.\n\n**On workspace restart** (user closes and reopens later):\n- The sandbox filesystem persists (files, installed packages, Docker image cache all survive)\n- Only processes die — the platform looks up the saved command and re-executes it\n- The preview URL auto-recovers\n\n**Implication:** Write commands that are **idempotent** — they must work whether run for the first time or re-run after a restart. The platform handles the rest. Docker image cache survives restart so rebuilds are fast (~2-5s with warm cache).\n\n### The User's Own Data\n\nAn app that shows the user's prices, watchlists, holdings, news, brief, earnings or threads reads them through the LangAlpha kit instead of fetching them itself: inside LangAlpha the page around the app answers as the signed-in user and keeps the data live. Read `.agents\u002Fskills\u002Fdashboard-page\u002Freferences\u002Fdashboard-kit.md` for the components and `window.la`. Data the kit does not have comes from the app's backend, which imports `tools.*` as your code does when it runs directly on the computer; inside a Docker image it cannot (see Docker Gotchas).\n\nStart a backend that imports `tools.*` with `GetPreviewUrl` alone, not from Bash first: only the server `GetPreviewUrl` starts can import them, and a port already answering makes it skip that start and leave the Bash copy running without them. Check the app after the call; a sub-agent returns the serve details without starting it.\n\n### Sandbox Capabilities\n\nAll pre-installed in the Daytona sandbox snapshot — no `pip install` or `apt-get` needed:\n\n- **Python 3.12** + pandas, numpy, plotly, matplotlib, requests, httpx, yfinance\n- **FastAPI + uvicorn** (available via `fastmcp` transitive dependency)\n- **Node.js 24 + npm** (host sandbox) \u002F **Node.js 20** (Docker `apt install nodejs`) — scaffold Vite\u002FReact projects with `npm create vite@latest`\n- **Docker Engine** — for complex tier containerized dashboards (backend + frontend in one image)\n- **Playwright + Chromium** — available for verification testing\n\n## Workflow\n\n### Step 1: Clarify Scope\n\nBefore writing any code:\n- What data? (specific tickers, sector, portfolio, screener results)\n- What visualizations? (price chart, comparison table, heatmap, etc.)\n- Static snapshot or live refresh?\n- How complex? (determines simple vs complex tier)\n\n### Step 2: Fetch Data\n\nUse **YF MCP servers** as the default financial data source (no API keys needed):\n\n```python\nfrom tools.yf_price import get_stock_history, get_multiple_stocks_history\nfrom tools.yf_fundamentals import get_company_info, compare_valuations\nfrom tools.yf_analysis import get_analyst_price_targets, get_news\nfrom tools.yf_market import get_sector_info, screen_stocks\n```\n\nAlways fetch and validate data **before** writing any HTML\u002FReact code. Check for empty responses.\n\n### Step 3: Process Data\n\nUse pandas to clean, aggregate, and compute derived metrics:\n\n```python\nimport pandas as pd\nimport json\n\n# Fetch\nhistory = get_stock_history(\"AAPL\", period=\"1y\", interval=\"1d\")\ninfo = get_company_info(\"AAPL\")\n\n# Process\ndf = pd.DataFrame(history)\ndf['change_pct'] = df['close'].pct_change() * 100\n\n# Prepare for frontend\nchart_data = json.dumps({\n    \"dates\": df['date'].tolist(),\n    \"prices\": df['close'].tolist(),\n    \"volumes\": df['volume'].tolist(),\n})\n```\n\n### Step 4: Build Dashboard\n\n**Simple tier** — write a self-contained HTML file:\n\n```python\nhtml = f\"\"\"\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    \u003Ctitle>AAPL Dashboard\u003C\u002Ftitle>\n    \u003Cscript src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fchart.js@4\u002Fdist\u002Fchart.umd.min.js\">\u003C\u002Fscript>\n    \u003Cstyle>\n        \u002F* See references\u002Fui-components.md for the theme foundation CSS *\u002F\n    \u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cscript>const DATA = {chart_data};\u003C\u002Fscript>\n    \u003Cscript>\n        \u002F* Chart rendering code *\u002F\n    \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\"\"\"\n\nwith open(\"dashboard\u002Findex.html\", \"w\") as f:\n    f.write(html)\n```\n\n**FastAPI + HTML tier** — write a FastAPI server with API routes + `StaticFiles` mount, and a single `static\u002Findex.html` with `fetch()` polling (see FastAPI + HTML Tier section below).\n\n**Complex tier** — scaffold a FastAPI + Vite\u002FReact project (see Complex Tier section below).\n\n### Step 5: Verify Before Serving\n\n**Tier 1 — Syntax check (required, \u003C 1 second)**\n\nExtract `\u003Cscript>` blocks from the HTML and check with `node --check`:\n\n```python\nimport re, subprocess, tempfile, os\n\nwith open(\"dashboard\u002Findex.html\") as f:\n    html = f.read()\n\nscripts = re.findall(r'\u003Cscript(?![^>]*src)[^>]*>(.*?)\u003C\u002Fscript>', html, re.DOTALL)\nfor i, src in enumerate(scripts):\n    with tempfile.NamedTemporaryFile(suffix=\".js\", mode=\"w\", delete=False) as tmp:\n        tmp.write(src)\n        tmp_path = tmp.name\n    result = subprocess.run([\"node\", \"--check\", tmp_path], capture_output=True, text=True)\n    os.unlink(tmp_path)\n    if result.returncode != 0:\n        raise RuntimeError(f\"JS syntax error in script block {i+1}:\\n{result.stderr}\")\nprint(\"Syntax check passed\")\n```\n\nAlso run the CSP self-check grep from the CSP section above.\n\n**Tier 2 — Browser verification (recommended for interactive dashboards)**\n\nRun when the dashboard has buttons, filters, or tabs. Skip for static data displays.\n\nAfter `GetPreviewUrl` starts the server, run a Playwright check to catch runtime errors:\n\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch()\n    page = browser.new_page()\n    js_errors = []\n    page.on(\"pageerror\", lambda exc: js_errors.append(str(exc)))\n    page.goto(\"http:\u002F\u002F127.0.0.1:8050\u002F\", wait_until=\"networkidle\", timeout=20000)\n    assert not js_errors, f\"JS runtime errors: {js_errors}\"\n    assert len(page.locator(\"body\").inner_text().strip()) > 20, \"Page appears blank\"\n    page.screenshot(path=\"dashboard\u002Fverify-screenshot.png\", full_page=True)\n    browser.close()\nprint(\"Browser verification passed\")\n```\n\nSee [references\u002Fverification.md](references\u002Fverification.md) for extended templates with button-click testing and API response validation.\n\n### Step 6: Serve & Expose\n\n```python\n# Simple tier\nGetPreviewUrl(port=8050, command=\"cd dashboard && python -m http.server 8050 --bind 0.0.0.0\", title=\"AAPL Dashboard\")\n\n# FastAPI + HTML tier \u002F Complex tier\nGetPreviewUrl(port=8050, command=\"bash dashboard\u002Fstart.sh\", title=\"Stock Dashboard\")\n```\n\n**Local verification before `GetPreviewUrl`** — if you need the server running for Playwright verification, start it from Bash with `&` and its output sent to a file (see above). Do NOT use `subprocess.Popen` from `execute_code` — the process becomes a zombie when the tool-call shell exits.\n\n### Step 7: Iterate\n\nAfter the user sees the preview, adjust layout, data, or charts based on feedback.\n\n## Data Integration — YF MCP Servers\n\nDefault data sources for common dashboard needs:\n\n| Need | MCP Server | Function | Key params |\n|------|------------|----------|------------|\n| Price history | `yf_price` | `get_stock_history` | `ticker, period=\"1y\", interval=\"1d\"` |\n| Multi-stock prices | `yf_price` | `get_multiple_stocks_history` | `tickers=[\"AAPL\",\"MSFT\"]` |\n| Dividends & splits | `yf_price` | `get_dividends_and_splits` | `ticker` |\n| Company profile | `yf_fundamentals` | `get_company_info` | `ticker` |\n| Income statement | `yf_fundamentals` | `get_income_statement` | `ticker, quarterly=True` |\n| Balance sheet | `yf_fundamentals` | `get_balance_sheet` | `ticker, quarterly=True` |\n| Cash flow | `yf_fundamentals` | `get_cash_flow` | `ticker, quarterly=True` |\n| Valuation comps | `yf_fundamentals` | `compare_valuations` | `tickers=[\"AAPL\",\"MSFT\",\"GOOGL\"]` |\n| Financial comps | `yf_fundamentals` | `compare_financials` | `tickers, statement_type=\"income\"` |\n| Earnings data | `yf_fundamentals` | `get_earnings_data` | `ticker` |\n| Analyst targets | `yf_analysis` | `get_analyst_price_targets` | `ticker` |\n| Recommendations | `yf_analysis` | `get_analyst_recommendations` | `ticker` |\n| Upgrades\u002Fdowngrades | `yf_analysis` | `get_upgrades_downgrades` | `ticker` |\n| Earnings estimates | `yf_analysis` | `get_earnings_estimates` | `ticker` |\n| Revenue estimates | `yf_analysis` | `get_revenue_estimates` | `ticker` |\n| Growth estimates | `yf_analysis` | `get_growth_estimates` | `ticker` |\n| Institutional holders | `yf_analysis` | `get_institutional_holders` | `ticker` |\n| Insider transactions | `yf_analysis` | `get_insider_transactions` | `ticker` |\n| ESG data | `yf_analysis` | `get_sustainability_data` | `ticker` |\n| News | `yf_analysis` | `get_news` | `ticker, count=10` |\n| Ticker search | `yf_market` | `search_tickers` | `query, max_results=8` |\n| Market status | `yf_market` | `get_market_status` | `market=\"US\"` |\n| Stock screener | `yf_market` | `screen_stocks` | `filters, sort_field, count` |\n| Predefined screens | `yf_market` | `get_predefined_screen` | `screen_name` (day_gainers, most_actives, etc.) |\n| Earnings calendar | `yf_market` | `get_earnings_calendar` | `start, end` (YYYY-MM-DD) |\n| Sector info | `yf_market` | `get_sector_info` | `sector_key` (technology, healthcare, etc.) |\n| Industry info | `yf_market` | `get_industry_info` | `industry_key` |\n\n### Yahoo Finance Field Conventions\n\nMany fields are already in display units — do NOT multiply by 100:\n\n| Field | Unit | Example | Note |\n|-------|------|---------|------|\n| `regularMarketChangePercent` | % (not decimal) | `0.389` = +0.39% | Do NOT multiply by 100 |\n| `dividendYield` | % (not decimal) | `0.41` = 0.41% | Same convention |\n| `marketCap` | Absolute USD | `3.71e12` | Divide by 1e9 for $B display |\n| `trailingPE` | Ratio | `31.98` | Display directly |\n\n### `get_predefined_screen` Response Structure\n\nQuotes are nested — not at the top level:\n\n```python\nresult = get_predefined_screen(\"day_gainers\")\nquotes = result[\"data\"][\"quotes\"]  # nested at result[\"data\"][\"quotes\"], NOT result[\"quotes\"]\n# Each quote: symbol, regularMarketPrice, regularMarketChangePercent, marketCap, ...\n```\n\n### Direct yfinance Usage (Docker \u002F without MCP)\n\nInside Docker containers, MCP tool modules are unavailable. Use `yfinance` directly:\n\n```python\nimport yfinance as yf\n\n# Stock screener (yfinance 1.2.0+)\nresult = yf.screen(\"day_gainers\", count=5)\nquotes = result.get(\"quotes\", [])\n\n# Stock data\nticker = yf.Ticker(\"AAPL\")\ninfo = ticker.info\nhist = ticker.history(period=\"1y\")\n```\n\n## UI Design Rules\n\nRead `.agents\u002Fskills\u002Fui-design\u002FSKILL.md` for design quality (typography, color, avoiding generic AI aesthetics).\n\n### Theming\n\nA dashboard must **follow the theme it is viewed in** — never ship a fixed palette:\n\n- **In the app**, the viewer injects theme tokens (`--color-bg-page`, `--color-text-primary`, …) that flip with the user's light\u002Fdark setting.\n- **Standalone** (downloaded and opened directly) no tokens exist, so the dashboard follows the OS via `prefers-color-scheme`.\n\nThe **Theme Foundation block** in [references\u002Fui-components.md](references\u002Fui-components.md) §1 implements both: each bridge variable reads the app token first and falls back to an OS-adaptive value. Include that block **verbatim** in every dashboard, then style only with the bridge variables. Canvas\u002FSVG chart code can't read `var()` — read the resolved value with the `pick()` helper (see [references\u002Fchart-patterns.md](references\u002Fchart-patterns.md)). **Never hardcode a palette hex in styles.**\n\n| Bridge variable | App token | Role |\n|---|---|---|\n| `--bg-page` | `--color-bg-page` | Page background |\n| `--bg-card` | `--color-bg-card` | Card \u002F panel background |\n| `--bg-elevated` | `--color-bg-elevated` | Raised surface (menus, popovers) |\n| `--bg-subtle` | `--color-bg-subtle` | Zebra rows, quiet fills |\n| `--bg-hover` | `--color-bg-hover` | Hover highlight, table headers |\n| `--text-primary` | `--color-text-primary` | Primary text |\n| `--text-secondary` | `--color-text-secondary` | Labels, captions, axis ticks |\n| `--text-tertiary` | `--color-text-tertiary` | De-emphasized text |\n| `--border` | `--color-border-muted` | Borders, grid lines |\n| `--accent` | `--color-accent-primary` | Links, primary series, active tab |\n| `--positive` | `--color-profit` | Gains |\n| `--negative` | `--color-loss` | Losses |\n| `--warning` | `--color-warning` | Cautions |\n\nLight and dark literal values live **only** in the foundation block — don't restate them elsewhere.\n\n### Layout\n\n- KPI cards in a row at top (price, change, volume, market cap)\n- Charts in a responsive 2-column grid below\n- Tables full-width at bottom\n- No horizontal scroll — everything fits the iframe width\n- Use CSS Grid with `auto-fit` and `minmax()` for responsive columns\n\n### Typography\n\n```css\nfont-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n```\n\n| Element | Size |\n|---------|------|\n| Page title (h1) | `1.5rem` |\n| Section title (h2) | `1.125rem` |\n| Body text | `0.875rem` |\n| Labels \u002F captions | `0.75rem` |\n| KPI value | `1.75rem` (bold) |\n\n### Financial Data Formatting\n\n- **Prices**: 2 decimal places with `$` prefix (`$182.52`)\n- **Percentages**: 2 decimal places with `%` suffix, color-coded green\u002Fred (`+2.34%` \u002F `-1.56%`)\n- **Large numbers**: Abbreviated with suffix (`$2.87T`, `$142.5B`, `$3.2M`)\n- **Volumes**: Comma-separated (`12,345,678`) or abbreviated (`12.3M`)\n- **Dates**: `MMM DD, YYYY` format (`Mar 15, 2026`)\n\nSee [references\u002Fui-components.md](references\u002Fui-components.md) for complete CSS and component code.\n\n## FastAPI + HTML Tier — Project Structure\n\nFor live-data dashboards without React. FastAPI serves API endpoints and static HTML directly — no npm, no build step. Use the **copy-ready template files** with `.fastapi-html` suffix in `references\u002F`:\n\n```\n\u003Ctask>\u002F\n├── Dockerfile           # cp references\u002FDockerfile.fastapi-html Dockerfile\n├── start.sh             # cp references\u002Fstart.sh start.sh\n├── server\u002F\n│   ├── main.py          # cp references\u002Fserver-main.fastapi-html.py server\u002Fmain.py\n│   └── requirements.txt # cp references\u002Frequirements.txt server\u002Frequirements.txt\n└── static\u002F\n    └── index.html       # Single HTML file with fetch() polling\n```\n\n### Setup Workflow\n\n1. **Copy template files** — all four `cp` commands above, then add your API routes to `server\u002Fmain.py`\n2. **Add your Python deps** to `server\u002Frequirements.txt` (append pandas, yfinance, etc.)\n3. **Write `static\u002Findex.html`** with `fetch()` calls to your API routes for live data\n4. **Serve**: `GetPreviewUrl(port=8050, command=\"bash \u003Ctask>\u002Fstart.sh\", title=\"Dashboard\")`\n\n### Template Files\n\n**`Dockerfile`** ([references\u002FDockerfile.fastapi-html](references\u002FDockerfile.fastapi-html)) — Python 3.12-slim only (no Node\u002Fnpm). Uses `uv pip install` for fast deps.\n\n**`server\u002Fmain.py`** ([references\u002Fserver-main.fastapi-html.py](references\u002Fserver-main.fastapi-html.py)) — FastAPI skeleton with CORS, `HEAD \u002F`, `\u002Fhealthz`, and `StaticFiles` mount for `static\u002F` directory. `StaticFiles` is the last mount (catches all unmatched routes).\n\n**`start.sh`** — same `references\u002Fstart.sh` template (works unchanged for all Docker tiers).\n\n### Key Differences from Complex Tier\n\n- **No `frontend\u002F` directory** — HTML lives in `static\u002F`\n- **No npm\u002FNode** — Dockerfile uses `python:3.12-slim` only\n- **`StaticFiles` mount** — serves `static\u002F` directory with `html=True` (auto-serves `index.html`)\n- **`fetch()` for live data** — HTML uses `setInterval(() => fetch('\u002Fapi\u002Fdata').then(...), 30000)` for auto-refresh\n- **No SPA routing** — single `index.html`, no client-side router needed\n\n## Complex Tier — Project Structure\n\nWhen using FastAPI + Vite\u002FReact, scaffold this structure using the **copy-ready template files** in `references\u002F`:\n\n```\n\u003Ctask>\u002F\n├── Dockerfile           # Copy from references\u002FDockerfile\n├── start.sh             # Copy from references\u002Fstart.sh\n├── server\u002F\n│   ├── main.py          # Copy from references\u002Fserver-main.py, add your API routes\n│   ├── requirements.txt # Copy from references\u002Frequirements.txt, add your deps\n│   ├── routes\u002F          # API route modules (stocks.py, sectors.py)\n│   └── models.py        # Pydantic response models\n├── frontend\u002F\n│   ├── package.json     # Vite + React + chart libraries\n│   ├── vite.config.js   # Copy from references\u002Fvite.config.js\n│   ├── index.html\n│   └── src\u002F\n│       ├── App.jsx      # Main app with routing\u002Ftabs\n│       ├── components\u002F  # Chart, KPI, Table components\n│       ├── hooks\u002F       # useStockData, useSectorData, etc.\n│       └── utils\u002F       # formatters, color helpers\n└── verify.py            # Copy from references\u002Fverification.md (optional)\n```\n\n### Setup Workflow\n\n1. **Copy template files** from `references\u002F` into `\u003Ctask>\u002F` — they work with zero modifications for port 8050\n2. **Add your API routes** to `server\u002Fmain.py` (the template includes CORS, `HEAD \u002F`, `\u002Fhealthz`, and static file serving)\n3. **Add your Python deps** to `server\u002Frequirements.txt` (template includes fastapi + uvicorn)\n4. **Write frontend code** in `frontend\u002Fsrc\u002F` (vite.config.js template proxies `\u002Fapi` to backend on port 8051)\n5. **Serve**: `GetPreviewUrl(port=8050, command=\"bash \u003Ctask>\u002Fstart.sh\", title=\"Dashboard\")`\n\n### Template Files\n\n**`Dockerfile`** ([references\u002FDockerfile](references\u002FDockerfile)) — Python 3.12 + Node + uv + tzdata. Builds frontend at image time, serves static files from FastAPI. Uses `uv pip install` for fast dependency installation.\n\n**`start.sh`** ([references\u002Fstart.sh](references\u002Fstart.sh)) — Cold-boot safe Docker wrapper. Starts dockerd if needed, builds image (uses layer cache on re-runs), removes old container, starts new one, health-checks with log dump on failure. Env var overrides: `PORT` (default 8050), `NAME` (default \"dashboard\").\n\n**`server\u002Fmain.py`** ([references\u002Fserver-main.py](references\u002Fserver-main.py)) — FastAPI skeleton with CORS, `HEAD \u002F` (liveness for platform proxy), `\u002Fhealthz`, SPA catch-all route for client-side routing (`\u002Ftab\u002Fnews`, `\u002Fstocks\u002FAAPL` → `index.html`), and a 503 fallback if the frontend build is missing.\n\n**`server\u002Frequirements.txt`** ([references\u002Frequirements.txt](references\u002Frequirements.txt)) — Minimal: fastapi + uvicorn. Append project-specific packages (pandas, yfinance, etc.).\n\n**`frontend\u002Fvite.config.js`** ([references\u002Fvite.config.js](references\u002Fvite.config.js)) — Vite + React with `\u002Fapi` proxy to backend on port 8051.\n\n> **Critical: Vite proxy is dev-only.** The `proxy` setting in `vite.config.js` only applies during `npm run dev`. The production build outputs plain static files with no proxy. In the Docker image, FastAPI serves both the built SPA from `frontend\u002Fdist\u002F` and all `\u002Fapi\u002F*` routes from the **same port**. The reference `server-main.py` template already does this correctly — do NOT use a two-process architecture with separate static file server and API server.\n\n### Docker Gotchas\n\n- **MCP tools are host-only**: The `tools\u002F` modules (e.g., `from tools.yf_price import ...`) exist only in the host workspace Python environment — they are NOT copied into the Docker image. FastAPI server code inside Docker must call `yfinance` directly. Add `yfinance` to `server\u002Frequirements.txt`\n- **`--network host`**: Required for the container to reach external APIs (yfinance, MCP servers). Already set in `start.sh` template\n- **`tzdata`**: Required for yfinance timezone handling. Already in `Dockerfile` template\n- **Image cache**: Persists across workspace restarts. First build: 30-60s. Subsequent builds: ~2-5s (cached layers)\n- **Logs**: Use `docker logs dashboard` to debug startup failures\n- **Fallback without Docker**: If Docker is unavailable, build the frontend and run FastAPI directly:\n  ```bash\n  fuser -k 8050\u002Ftcp 2>\u002Fdev\u002Fnull || true\n  cd frontend && npm install --prefer-offline && npm run build && cd ..\n  cd server && uvicorn main:app --host 0.0.0.0 --port 8050\n  ```\n\n## Chart Libraries\n\n### Simple Tier (CDN-loaded, no install)\n\n| Library | CDN URL | Best for |\n|---------|---------|----------|\n| **Chart.js** | `https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fchart.js@4\u002Fdist\u002Fchart.umd.min.js` | Line, bar, pie, doughnut, area |\n| **Plotly.js** | `https:\u002F\u002Fcdn.plot.ly\u002Fplotly-2.35.2.min.js` | Candlestick, heatmap, treemap |\n| **Lightweight Charts** | `https:\u002F\u002Funpkg.com\u002Flightweight-charts@4\u002Fdist\u002Flightweight-charts.standalone.production.js` | TradingView-style candlestick |\n\nDefault to **Chart.js**. Use Plotly for candlesticks\u002Fheatmaps. Lightweight Charts only for TradingView-style.\n\n### Complex Tier (npm packages)\n\n| Library | Package | Best for |\n|---------|---------|----------|\n| **Recharts** | `recharts` | Composable React charts — line, bar, area, pie |\n| **Plotly React** | `react-plotly.js plotly.js` | Candlestick, heatmap, treemap |\n| **Lightweight Charts** | `lightweight-charts` | TradingView-style financial charts |\n\nDefault to **Recharts**. Use Plotly for advanced financial charts.\n\nSee [references\u002Fchart-patterns.md](references\u002Fchart-patterns.md) for ready-to-use code snippets.\n\n## Common Dashboard Patterns\n\n### 1. Single Stock Dashboard\n\n**Data**: `get_stock_history`, `get_company_info`, `get_analyst_price_targets`, `get_news`\n\nLayout:\n- KPI row: current price, day change %, 52-week range, market cap, P\u002FE\n- Price chart (line\u002Fcandlestick) with volume bars\n- Analyst price target range (horizontal bar)\n- Recent news list\n\n### 2. Multi-Stock Comparison\n\n**Data**: `get_multiple_stocks_history`, `compare_valuations`, `compare_financials`\n\nLayout:\n- Normalized price overlay chart (base 100)\n- Performance bar chart (YTD, 1Y, 3Y returns)\n- Valuation comparison table (P\u002FE, EV\u002FEBITDA, P\u002FB, etc.)\n- Revenue\u002Fearnings growth comparison\n\n### 3. Sector Heatmap\n\n**Data**: `get_sector_info`, `screen_stocks` with sector filters, `get_predefined_screen`\n\nLayout:\n- Treemap colored by daily\u002Fweekly performance\n- Sector summary cards (top movers, average P\u002FE)\n- Top gainers\u002Flosers table\n- Sector rotation chart\n\n### 4. Earnings Tracker\n\n**Data**: `get_earnings_calendar`, `get_earnings_data`, `get_earnings_estimates`\n\nLayout:\n- Calendar view with upcoming earnings dates\n- Beat\u002Fmiss history chart (bar chart with surprise %)\n- EPS estimate vs actual trend line\n- Revenue estimate revision chart\n\n### 5. Portfolio Monitor\n\n**Data**: the holdings, their live prices and sectors through the kit (`portfolio.holdings`, `la.stream`, `profiles.get`); `compare_valuations` and history from the backend for what the kit does not have\n\nLayout:\n- Holdings table (ticker, shares, price, value, weight, day P&L)\n- Allocation pie chart (by sector\u002Fstock)\n- Total portfolio value line chart\n- Sector exposure bar chart\n\n## Best Practices\n\n### General\n\n- **Data-first**: Fetch and validate the data the backend serves before writing any HTML\u002FReact code (the kit's data comes live in the page)\n- **Fail gracefully**: If a ticker is invalid or API returns empty, show \"No data available\" — don't crash\n- **No console errors**: Verify chart rendering works before calling `GetPreviewUrl`\n- **Responsive**: CSS Grid `auto-fit` for layouts. No horizontal scroll at any width\n- **Performance**: Resample data if > 1000 rows. Don't load unused chart libraries\n\n### Simple Tier\n\n- **Embed data as JSON**: `\u003Cscript>const DATA = ${json.dumps(data)}\u003C\u002Fscript>` — never inline raw Python dicts\n- **Escape properly**: Always use `json.dumps()` with `ensure_ascii=False` for safe JSON embedding\n- **Self-contained**: All CSS in `\u003Cstyle>`, all JS in `\u003Cscript>`, libraries via CDN `\u003Cscript src=\"...\">`\n- **One HTML file**: Keep everything in a single `index.html` — eliminates path bugs\n\n### Complex Tier\n\n- **Separation of concerns**: FastAPI = data API, Vite\u002FReact = UI rendering\n- **Pydantic models**: Define response schemas for type safety\n- **Component per widget**: One React component per chart\u002Fcard\u002Ftable\n- **Shared hooks**: `useStockData(ticker)`, `useSectorData(key)` for data fetching\n- **Error boundaries**: Wrap chart components so one failure doesn't crash the whole page\n- **Single-port production**: Vite proxy is dev-only. In production Docker builds, FastAPI serves both `\u002Fapi\u002F*` and the SPA from one port\n- **`host: '0.0.0.0'`**: Both FastAPI and Vite must bind to `0.0.0.0`, not `127.0.0.1` or `localhost`\n\n## Error Handling & Debugging\n\n| Problem | Solution |\n|---------|----------|\n| `GetPreviewUrl` returns error | Port already in use — try a different port (8051, 8052, ...) |\n| Page is blank | Check for JS errors — ensure all `getElementById` targets exist |\n| Data is empty | Validate MCP tool response before embedding — check for `None` or empty lists |\n| Buttons\u002Finputs do nothing | CSP blocking inline handlers — replace `onclick=` etc. with `addEventListener`. Run CSP self-check |\n| FastAPI won't start | Ensure `host='0.0.0.0'` in `uvicorn.run()` |\n| Vite won't start | Ensure `--host 0.0.0.0` flag and check if port is free |\n| CORS errors | Add `CORSMiddleware` to FastAPI or use Vite proxy |\n| Charts don't render | CDN scripts must load before chart initialization — use `DOMContentLoaded` event |\n| Iframe shows \"refused to connect\" | Server not ready yet — add a small delay or retry logic |\n| HEAD \u002F returns 404 or 405 | Add `@app.head(\"\u002F\")` as its own function — don't stack with `\u002Fhealthz` (use `server-main.py` template) |\n| SPA deep route returns 404 | Add catch-all `@app.get(\"\u002F{full_path:path}\")` that serves `index.html` for non-file paths (use `server-main.py` template) |\n| `start.sh` fails on restart | Ensure idempotent: dockerd startup check, `docker rm -f` before `docker run` (use `start.sh` template) |\n| Docker: yfinance timezone error | Add `tzdata` package to Dockerfile (included in template) |\n| Docker: can't reach external APIs | Use `--network host` flag (included in `start.sh` template) |\n| `GetPreviewUrl` not found \u002F `NameError` | Tool only available to main agent runtime — sub-agents start the server from Bash with `&` and its output sent to a file, then report port\u002Fcommand\u002Ftitle back |\n| Playwright `ERR_CONNECTION_REFUSED` | Use `127.0.0.1:PORT` not `localhost` — sandbox resolves `localhost` to IPv6 (`::1`) first |\n| Background server died \u002F `\u003Cdefunct>` | Start the server from Bash with `&` and its output sent to a file — do NOT use `subprocess.Popen` from `execute_code` (process becomes zombie when tool-call shell exits) |\n| `ModuleNotFoundError: tools.*` in Docker | MCP tools are host-only — use `yfinance` directly inside Docker containers |\n\n## Quality Checklist\n\nBefore calling `GetPreviewUrl`:\n\n**Data & Code**\n- [ ] All data fetched and validated (no empty dataframes or None values)\n- [ ] Files written to `\u003Ctask>\u002F` directory\n- [ ] JSON data properly escaped with `json.dumps()`\n- [ ] All chart containers exist in HTML before JS tries to reference them\n\n**CSP Safety**\n- [ ] No inline event handlers (`onclick`, `oninput`, `onchange`, etc.) — all events via `addEventListener`\n- [ ] No `eval()`, `new Function()`, or string-based `setTimeout()`\n- [ ] No `javascript:` URLs\n- [ ] CSP self-check grep passes (no matches)\n\n**Verification**\n- [ ] Tier 1: JS syntax check passed (`node --check` on extracted script blocks)\n- [ ] Tier 2: Playwright verification passed (for interactive dashboards with buttons\u002Ffilters\u002Ftabs)\n\n**Serving**\n- [ ] Server binds to `0.0.0.0` (not `127.0.0.1` or `localhost`)\n- [ ] Correct port used (default 8050)\n- [ ] Command passed to `GetPreviewUrl` is idempotent (works on re-run after restart)\n- [ ] Complex tier: `start.sh` and `Dockerfile` copied from templates\n- [ ] Complex tier: FastAPI includes `HEAD \u002F` endpoint (use `server-main.py` template)\n\n**UI Quality**\n- [ ] Theme foundation block included verbatim; only bridge variables (and `pick()` for canvas colors) used — no palette hexes in styles\n- [ ] Renders correctly in **both** light and dark\n- [ ] Responsive layout — no horizontal scroll\n- [ ] Financial numbers properly formatted (currency, %, abbreviations)\n- [ ] Title passed to `GetPreviewUrl` is descriptive (e.g., \"AAPL Stock Dashboard\", not \"Preview\")\n","# Interactive Web Apps\n\nBuild interactive web apps inside the sandbox and expose them to the user via `GetPreviewUrl`: dashboards, trackers, monitors, screeners, calculators and tools, live visualizations, multi-page apps. Anything the user opens as an app, which needs a server running on the computer.\n\n## When to Use\n\nUse this skill for a **live, served web app** — one that needs a running server, not a single file:\n\n- User asks for a **dashboard**, **tracker**, or **monitor** that **refreshes live data** (polling, auto-update)\n- The app needs **server-side logic** — filtering\u002Fscreening over a large dataset, on-demand fetches, computed endpoints\n- **Multi-page \u002F routed** apps, or anything that needs React-level component interactivity\n- The dataset is **too large to embed** in a single HTML file\n- User explicitly says \"preview\", \"web view\", \"web app\", or wants it running at a URL\n\n**Do NOT use if:**\n- User wants **their LangAlpha Dashboard**, the page they see when they open LangAlpha, built or changed. That's `.agents\u002Fskills\u002Fdashboard-page\u002FSKILL.md`: a page LangAlpha keeps a copy of, which opens while the computer is off. A running app cannot. The user can still set a running app as their Dashboard from the file panel; it opens only while the computer is on.\n- User wants a **self-contained HTML report** — even an *interactive* one (sortable tables, tabs, hover\u002Fzoom charts) over a **data snapshot**. That's `.agents\u002Fskills\u002Fhtml-report\u002FSKILL.md`: one file in the task directory, keepable, printable, PDF-exportable, share-linkable. Interactivity by itself does **not** require a dashboard.\n- User wants a **static chart image** → matplotlib\u002Fplotly `savefig`.\n- User wants an **in-chat figure** → `inline-widget` (`ShowWidget`).\n\n### Dashboard vs. HTML Report\n\nBoth can be interactive, so the divide is **live served app vs. self-contained snapshot file**, not static vs. interactive:\n\n| | interactive-dashboard (this skill) | html-report |\n|---|---|---|\n| Delivery | A **running server**, exposed via `GetPreviewUrl` | One **`.html` file** in `\u003Ctask_name>\u002F` |\n| Data | **Live \u002F refreshing**, fetched from a backend; large datasets OK | A **snapshot** embedded in the file |\n| Interactivity | Full app — routing, server-side filtering, live updates | Client-side over the snapshot — sort, filter, tabs, chart hover\u002Fzoom |\n| Keep \u002F print \u002F share | A URL, live only while the workspace runs | Downloadable, PDF-exportable, share-linkable as one artifact |\n| Pick when | Data must be live, or compute\u002Fscale needs a server | The answer is a deliverable the user keeps |\n\n## Architecture\n\nChoose the tier based on complexity:\n\n| Tier | When | Stack | Serve command |\n|------|------|-------|---------------|\n| **Simple** | Snapshot-at-load data, few charts, no backend logic (still served via preview URL) | Self-contained HTML + CDN libs | `python -m http.server 8050 --bind 0.0.0.0` |\n| **FastAPI + HTML** | Live data refresh, server-side logic, no React needed | FastAPI serves `static\u002F` + `fetch()` polling | `bash start.sh` |\n| **Complex** | Filtering, routing, component interactivity, multi-page | FastAPI backend + Vite\u002FReact frontend | `bash start.sh` |\n\n**Decision rule:** Start with Simple. Escalate to FastAPI + HTML when user needs live data refresh or server-side logic. Escalate to Complex only when user needs React-level component interactivity, client-side routing, or a multi-page SPA.\n\n**Port convention:** Use port **8050** (default). Range 8050-8059 for dashboards.\n\n### CSP \u002F Iframe Safety\n\nThe preview iframe enforces Content Security Policy (CSP). Certain patterns are **silently blocked** — no error banner, just dead UI elements. Always use the safe alternatives:\n\n| Blocked pattern | Safe alternative |\n|-----------------|-----------------|\n| `\u003Cbutton onclick=\"fn()\">` | `el.addEventListener('click', fn)` |\n| `\u003Cdiv onmouseover=\"fn()\">` | `el.addEventListener('mouseover', fn)` |\n| Any `on*=\"...\"` HTML attribute | `el.addEventListener(event, fn)` |\n| `innerHTML` with `onclick` | `document.createElement()` + `addEventListener` |\n| `eval(\"code\")` | Direct function calls |\n| `new Function(\"code\")` | Named function declarations |\n| `setTimeout(\"code string\", ms)` | `setTimeout(fn, ms)` (function reference) |\n| `\u003Ca href=\"javascript:...\">` | `\u003Ca href=\"#\" data-action=\"...\">` + `addEventListener` |\n\n**Quick self-check** — run before serving to catch violations:\n\n```python\nimport subprocess\nresult = subprocess.run(\n    [\"grep\", \"-rnE\", r'on(click|input|change|focus|blur|submit|load|error|mouse|key)\\s*=',\n     \"dashboard\u002F\"],\n    capture_output=True, text=True\n)\nif result.stdout.strip():\n    raise RuntimeError(f\"CSP-unsafe inline handlers found:\\n{result.stdout}\")\n```\n\n**Template literal hygiene** — when building HTML strings in JS template literals, CSS semicolons inside `${}` expressions cause silent parse failures:\n\n```javascript\n\u002F\u002F BAD — semicolon inside ${} terminates the expression early\nconst el = `\u003Cdiv style=\"color:${positive ? 'green' : 'red'; font-weight:600}\">`;\n\n\u002F\u002F GOOD — close the expression first, then continue the attribute string\nconst el = `\u003Cdiv style=\"color:${positive ? 'green' : 'red'};font-weight:600\">`;\n```\n\nRule: **never put a CSS semicolon inside `${}`** — always close `}` before the semicolon.\n\n### How Preview Serving Works\n\n`GetPreviewUrl` is a **platform-level tool** available only to the main agent runtime. It is NOT a Python function — do not `import` it or call it from `execute_code`. The agent invokes it as a tool call.\n\nWhen you call `GetPreviewUrl(port, command, title)`:\n\n1. The **command is persisted to the database** automatically\n2. The platform starts the command in a dedicated sandbox session for that port\n3. It polls until the port is listening, then generates a signed URL\n4. If the port is **already reachable**, the command start is skipped entirely\n\n**Sub-agent fallback:** Sub-agents cannot call `GetPreviewUrl`. Instead, build the dashboard files, start the server for verification, then return the serve details so the orchestrating agent can call `GetPreviewUrl`.\n\n**All tiers:** start the server from Bash with `&` and its output sent to a file, so the call returns at once and the server keeps running:\n\n```bash\n# Simple tier\ncd \u003Ctask> && nohup python -m http.server 8050 --bind 0.0.0.0 > server.log 2>&1 &\n\n# Docker tiers\ncd \u003Ctask> && nohup bash start.sh > server.log 2>&1 &\n```\n\nThen verify it's up in a separate (foreground) Bash call:\n\n```bash\nfor i in $(seq 1 15); do curl -sf http:\u002F\u002F127.0.0.1:8050\u002F > \u002Fdev\u002Fnull && echo \"Server ready\" && exit 0 || sleep 1; done; echo \"FAIL\"; exit 1\n```\n\nThen return **all three fields** to the orchestrating agent (it needs the command for DB persistence \u002F restart recovery):\n\n```\nport: 8050\ncommand: \"cd \u003Ctask> && python -m http.server 8050 --bind 0.0.0.0\"  # or \"bash \u003Ctask>\u002Fstart.sh\"\ntitle: \"AAPL Stock Dashboard\"\n```\n\nThe orchestrating agent calls `GetPreviewUrl(port=8050, command=\"...\", title=\"...\")` which persists the command.\n\n**On workspace restart** (user closes and reopens later):\n- The sandbox filesystem persists (files, installed packages, Docker image cache all survive)\n- Only processes die — the platform looks up the saved command and re-executes it\n- The preview URL auto-recovers\n\n**Implication:** Write commands that are **idempotent** — they must work whether run for the first time or re-run after a restart. The platform handles the rest. Docker image cache survives restart so rebuilds are fast (~2-5s with warm cache).\n\n### The User's Own Data\n\nAn app that shows the user's prices, watchlists, holdings, news, brief, earnings or threads reads them through the LangAlpha kit instead of fetching them itself: inside LangAlpha the page around the app answers as the signed-in user and keeps the data live. Read `.agents\u002Fskills\u002Fdashboard-page\u002Freferences\u002Fdashboard-kit.md` for the components and `window.la`. Data the kit does not have comes from the app's backend, which imports `tools.*` as your code does when it runs directly on the computer; inside a Docker image it cannot (see Docker Gotchas).\n\nStart a backend that imports `tools.*` with `GetPreviewUrl` alone, not from Bash first: only the server `GetPreviewUrl` starts can import them, and a port already answering makes it skip that start and leave the Bash copy running without them. Check the app after the call; a sub-agent returns the serve details without starting it.\n\n### Sandbox Capabilities\n\nAll pre-installed in the Daytona sandbox snapshot — no `pip install` or `apt-get` needed:\n\n- **Python 3.12** + pandas, numpy, plotly, matplotlib, requests, httpx, yfinance\n- **FastAPI + uvicorn** (available via `fastmcp` transitive dependency)\n- **Node.js 24 + npm** (host sandbox) \u002F **Node.js 20** (Docker `apt install nodejs`) — scaffold Vite\u002FReact projects with `npm create vite@latest`\n- **Docker Engine** — for complex tier containerized dashboards (backend + frontend in one image)\n- **Playwright + Chromium** — available for verification testing\n\n## Workflow\n\n### Step 1: Clarify Scope\n\nBefore writing any code:\n- What data? (specific tickers, sector, portfolio, screener results)\n- What visualizations? (price chart, comparison table, heatmap, etc.)\n- Static snapshot or live refresh?\n- How complex? (determines simple vs complex tier)\n\n### Step 2: Fetch Data\n\nUse **YF MCP servers** as the default financial data source (no API keys needed):\n\n```python\nfrom tools.yf_price import get_stock_history, get_multiple_stocks_history\nfrom tools.yf_fundamentals import get_company_info, compare_valuations\nfrom tools.yf_analysis import get_analyst_price_targets, get_news\nfrom tools.yf_market import get_sector_info, screen_stocks\n```\n\nAlways fetch and validate data **before** writing any HTML\u002FReact code. Check for empty responses.\n\n### Step 3: Process Data\n\nUse pandas to clean, aggregate, and compute derived metrics:\n\n```python\nimport pandas as pd\nimport json\n\n# Fetch\nhistory = get_stock_history(\"AAPL\", period=\"1y\", interval=\"1d\")\ninfo = get_company_info(\"AAPL\")\n\n# Process\ndf = pd.DataFrame(history)\ndf['change_pct'] = df['close'].pct_change() * 100\n\n# Prepare for frontend\nchart_data = json.dumps({\n    \"dates\": df['date'].tolist(),\n    \"prices\": df['close'].tolist(),\n    \"volumes\": df['volume'].tolist(),\n})\n```\n\n### Step 4: Build Dashboard\n\n**Simple tier** — write a self-contained HTML file:\n\n```python\nhtml = f\"\"\"\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n    \u003Cmeta charset=\"UTF-8\">\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    \u003Ctitle>AAPL Dashboard\u003C\u002Ftitle>\n    \u003Cscript src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fchart.js@4\u002Fdist\u002Fchart.umd.min.js\">\u003C\u002Fscript>\n    \u003Cstyle>\n        \u002F* See references\u002Fui-components.md for the theme foundation CSS *\u002F\n    \u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cscript>const DATA = {chart_data};\u003C\u002Fscript>\n    \u003Cscript>\n        \u002F* Chart rendering code *\u002F\n    \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\"\"\"\n\nwith open(\"dashboard\u002Findex.html\", \"w\") as f:\n    f.write(html)\n```\n\n**FastAPI + HTML tier** — write a FastAPI server with API routes + `StaticFiles` mount, and a single `static\u002Findex.html` with `fetch()` polling (see FastAPI + HTML Tier section below).\n\n**Complex tier** — scaffold a FastAPI + Vite\u002FReact project (see Complex Tier section below).\n\n### Step 5: Verify Before Serving\n\n**Tier 1 — Syntax check (required, \u003C 1 second)**\n\nExtract `\u003Cscript>` blocks from the HTML and check with `node --check`:\n\n```python\nimport re, subprocess, tempfile, os\n\nwith open(\"dashboard\u002Findex.html\") as f:\n    html = f.read()\n\nscripts = re.findall(r'\u003Cscript(?![^>]*src)[^>]*>(.*?)\u003C\u002Fscript>', html, re.DOTALL)\nfor i, src in enumerate(scripts):\n    with tempfile.NamedTemporaryFile(suffix=\".js\", mode=\"w\", delete=False) as tmp:\n        tmp.write(src)\n        tmp_path = tmp.name\n    result = subprocess.run([\"node\", \"--check\", tmp_path], capture_output=True, text=True)\n    os.unlink(tmp_path)\n    if result.returncode != 0:\n        raise RuntimeError(f\"JS syntax error in script block {i+1}:\\n{result.stderr}\")\nprint(\"Syntax check passed\")\n```\n\nAlso run the CSP self-check grep from the CSP section above.\n\n**Tier 2 — Browser verification (recommended for interactive dashboards)**\n\nRun when the dashboard has buttons, filters, or tabs. Skip for static data displays.\n\nAfter `GetPreviewUrl` starts the server, run a Playwright check to catch runtime errors:\n\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch()\n    page = browser.new_page()\n    js_errors = []\n    page.on(\"pageerror\", lambda exc: js_errors.append(str(exc)))\n    page.goto(\"http:\u002F\u002F127.0.0.1:8050\u002F\", wait_until=\"networkidle\", timeout=20000)\n    assert not js_errors, f\"JS runtime errors: {js_errors}\"\n    assert len(page.locator(\"body\").inner_text().strip()) > 20, \"Page appears blank\"\n    page.screenshot(path=\"dashboard\u002Fverify-screenshot.png\", full_page=True)\n    browser.close()\nprint(\"Browser verification passed\")\n```\n\nSee [references\u002Fverification.md](references\u002Fverification.md) for extended templates with button-click testing and API response validation.\n\n### Step 6: Serve & Expose\n\n```python\n# Simple tier\nGetPreviewUrl(port=8050, command=\"cd dashboard && python -m http.server 8050 --bind 0.0.0.0\", title=\"AAPL Dashboard\")\n\n# FastAPI + HTML tier \u002F Complex tier\nGetPreviewUrl(port=8050, command=\"bash dashboard\u002Fstart.sh\", title=\"Stock Dashboard\")\n```\n\n**Local verification before `GetPreviewUrl`** — if you need the server running for Playwright verification, start it from Bash with `&` and its output sent to a file (see above). Do NOT use `subprocess.Popen` from `execute_code` — the process becomes a zombie when the tool-call shell exits.\n\n### Step 7: Iterate\n\nAfter the user sees the preview, adjust layout, data, or charts based on feedback.\n\n## Data Integration — YF MCP Servers\n\nDefault data sources for common dashboard needs:\n\n| Need | MCP Server | Function | Key params |\n|------|------------|----------|------------|\n| Price history | `yf_price` | `get_stock_history` | `ticker, period=\"1y\", interval=\"1d\"` |\n| Multi-stock prices | `yf_price` | `get_multiple_stocks_history` | `tickers=[\"AAPL\",\"MSFT\"]` |\n| Dividends & splits | `yf_price` | `get_dividends_and_splits` | `ticker` |\n| Company profile | `yf_fundamentals` | `get_company_info` | `ticker` |\n| Income statement | `yf_fundamentals` | `get_income_statement` | `ticker, quarterly=True` |\n| Balance sheet | `yf_fundamentals` | `get_balance_sheet` | `ticker, quarterly=True` |\n| Cash flow | `yf_fundamentals` | `get_cash_flow` | `ticker, quarterly=True` |\n| Valuation comps | `yf_fundamentals` | `compare_valuations` | `tickers=[\"AAPL\",\"MSFT\",\"GOOGL\"]` |\n| Financial comps | `yf_fundamentals` | `compare_financials` | `tickers, statement_type=\"income\"` |\n| Earnings data | `yf_fundamentals` | `get_earnings_data` | `ticker` |\n| Analyst targets | `yf_analysis` | `get_analyst_price_targets` | `ticker` |\n| Recommendations | `yf_analysis` | `get_analyst_recommendations` | `ticker` |\n| Upgrades\u002Fdowngrades | `yf_analysis` | `get_upgrades_downgrades` | `ticker` |\n| Earnings estimates | `yf_analysis` | `get_earnings_estimates` | `ticker` |\n| Revenue estimates | `yf_analysis` | `get_revenue_estimates` | `ticker` |\n| Growth estimates | `yf_analysis` | `get_growth_estimates` | `ticker` |\n| Institutional holders | `yf_analysis` | `get_institutional_holders` | `ticker` |\n| Insider transactions | `yf_analysis` | `get_insider_transactions` | `ticker` |\n| ESG data | `yf_analysis` | `get_sustainability_data` | `ticker` |\n| News | `yf_analysis` | `get_news` | `ticker, count=10` |\n| Ticker search | `yf_market` | `search_tickers` | `query, max_results=8` |\n| Market status | `yf_market` | `get_market_status` | `market=\"US\"` |\n| Stock screener | `yf_market` | `screen_stocks` | `filters, sort_field, count` |\n| Predefined screens | `yf_market` | `get_predefined_screen` | `screen_name` (day_gainers, most_actives, etc.) |\n| Earnings calendar | `yf_market` | `get_earnings_calendar` | `start, end` (YYYY-MM-DD) |\n| Sector info | `yf_market` | `get_sector_info` | `sector_key` (technology, healthcare, etc.) |\n| Industry info | `yf_market` | `get_industry_info` | `industry_key` |\n\n### Yahoo Finance Field Conventions\n\nMany fields are already in display units — do NOT multiply by 100:\n\n| Field | Unit | Example | Note |\n|-------|------|---------|------|\n| `regularMarketChangePercent` | % (not decimal) | `0.389` = +0.39% | Do NOT multiply by 100 |\n| `dividendYield` | % (not decimal) | `0.41` = 0.41% | Same convention |\n| `marketCap` | Absolute USD | `3.71e12` | Divide by 1e9 for $B display |\n| `trailingPE` | Ratio | `31.98` | Display directly |\n\n### `get_predefined_screen` Response Structure\n\nQuotes are nested — not at the top level:\n\n```python\nresult = get_predefined_screen(\"day_gainers\")\nquotes = result[\"data\"][\"quotes\"]  # nested at result[\"data\"][\"quotes\"], NOT result[\"quotes\"]\n# Each quote: symbol, regularMarketPrice, regularMarketChangePercent, marketCap, ...\n```\n\n### Direct yfinance Usage (Docker \u002F without MCP)\n\nInside Docker containers, MCP tool modules are unavailable. Use `yfinance` directly:\n\n```python\nimport yfinance as yf\n\n# Stock screener (yfinance 1.2.0+)\nresult = yf.screen(\"day_gainers\", count=5)\nquotes = result.get(\"quotes\", [])\n\n# Stock data\nticker = yf.Ticker(\"AAPL\")\ninfo = ticker.info\nhist = ticker.history(period=\"1y\")\n```\n\n## UI Design Rules\n\nRead `.agents\u002Fskills\u002Fui-design\u002FSKILL.md` for design quality (typography, color, avoiding generic AI aesthetics).\n\n### Theming\n\nA dashboard must **follow the theme it is viewed in** — never ship a fixed palette:\n\n- **In the app**, the viewer injects theme tokens (`--color-bg-page`, `--color-text-primary`, …) that flip with the user's light\u002Fdark setting.\n- **Standalone** (downloaded and opened directly) no tokens exist, so the dashboard follows the OS via `prefers-color-scheme`.\n\nThe **Theme Foundation block** in [references\u002Fui-components.md](references\u002Fui-components.md) §1 implements both: each bridge variable reads the app token first and falls back to an OS-adaptive value. Include that block **verbatim** in every dashboard, then style only with the bridge variables. Canvas\u002FSVG chart code can't read `var()` — read the resolved value with the `pick()` helper (see [references\u002Fchart-patterns.md](references\u002Fchart-patterns.md)). **Never hardcode a palette hex in styles.**\n\n| Bridge variable | App token | Role |\n|---|---|---|\n| `--bg-page` | `--color-bg-page` | Page background |\n| `--bg-card` | `--color-bg-card` | Card \u002F panel background |\n| `--bg-elevated` | `--color-bg-elevated` | Raised surface (menus, popovers) |\n| `--bg-subtle` | `--color-bg-subtle` | Zebra rows, quiet fills |\n| `--bg-hover` | `--color-bg-hover` | Hover highlight, table headers |\n| `--text-primary` | `--color-text-primary` | Primary text |\n| `--text-secondary` | `--color-text-secondary` | Labels, captions, axis ticks |\n| `--text-tertiary` | `--color-text-tertiary` | De-emphasized text |\n| `--border` | `--color-border-muted` | Borders, grid lines |\n| `--accent` | `--color-accent-primary` | Links, primary series, active tab |\n| `--positive` | `--color-profit` | Gains |\n| `--negative` | `--color-loss` | Losses |\n| `--warning` | `--color-warning` | Cautions |\n\nLight and dark literal values live **only** in the foundation block — don't restate them elsewhere.\n\n### Layout\n\n- KPI cards in a row at top (price, change, volume, market cap)\n- Charts in a responsive 2-column grid below\n- Tables full-width at bottom\n- No horizontal scroll — everything fits the iframe width\n- Use CSS Grid with `auto-fit` and `minmax()` for responsive columns\n\n### Typography\n\n```css\nfont-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n```\n\n| Element | Size |\n|---------|------|\n| Page title (h1) | `1.5rem` |\n| Section title (h2) | `1.125rem` |\n| Body text | `0.875rem` |\n| Labels \u002F captions | `0.75rem` |\n| KPI value | `1.75rem` (bold) |\n\n### Financial Data Formatting\n\n- **Prices**: 2 decimal places with `$` prefix (`$182.52`)\n- **Percentages**: 2 decimal places with `%` suffix, color-coded green\u002Fred (`+2.34%` \u002F `-1.56%`)\n- **Large numbers**: Abbreviated with suffix (`$2.87T`, `$142.5B`, `$3.2M`)\n- **Volumes**: Comma-separated (`12,345,678`) or abbreviated (`12.3M`)\n- **Dates**: `MMM DD, YYYY` format (`Mar 15, 2026`)\n\nSee [references\u002Fui-components.md](references\u002Fui-components.md) for complete CSS and component code.\n\n## FastAPI + HTML Tier — Project Structure\n\nFor live-data dashboards without React. FastAPI serves API endpoints and static HTML directly — no npm, no build step. Use the **copy-ready template files** with `.fastapi-html` suffix in `references\u002F`:\n\n```\n\u003Ctask>\u002F\n├── Dockerfile           # cp references\u002FDockerfile.fastapi-html Dockerfile\n├── start.sh             # cp references\u002Fstart.sh start.sh\n├── server\u002F\n│   ├── main.py          # cp references\u002Fserver-main.fastapi-html.py server\u002Fmain.py\n│   └── requirements.txt # cp references\u002Frequirements.txt server\u002Frequirements.txt\n└── static\u002F\n    └── index.html       # Single HTML file with fetch() polling\n```\n\n### Setup Workflow\n\n1. **Copy template files** — all four `cp` commands above, then add your API routes to `server\u002Fmain.py`\n2. **Add your Python deps** to `server\u002Frequirements.txt` (append pandas, yfinance, etc.)\n3. **Write `static\u002Findex.html`** with `fetch()` calls to your API routes for live data\n4. **Serve**: `GetPreviewUrl(port=8050, command=\"bash \u003Ctask>\u002Fstart.sh\", title=\"Dashboard\")`\n\n### Template Files\n\n**`Dockerfile`** ([references\u002FDockerfile.fastapi-html](references\u002FDockerfile.fastapi-html)) — Python 3.12-slim only (no Node\u002Fnpm). Uses `uv pip install` for fast deps.\n\n**`server\u002Fmain.py`** ([references\u002Fserver-main.fastapi-html.py](references\u002Fserver-main.fastapi-html.py)) — FastAPI skeleton with CORS, `HEAD \u002F`, `\u002Fhealthz`, and `StaticFiles` mount for `static\u002F` directory. `StaticFiles` is the last mount (catches all unmatched routes).\n\n**`start.sh`** — same `references\u002Fstart.sh` template (works unchanged for all Docker tiers).\n\n### Key Differences from Complex Tier\n\n- **No `frontend\u002F` directory** — HTML lives in `static\u002F`\n- **No npm\u002FNode** — Dockerfile uses `python:3.12-slim` only\n- **`StaticFiles` mount** — serves `static\u002F` directory with `html=True` (auto-serves `index.html`)\n- **`fetch()` for live data** — HTML uses `setInterval(() => fetch('\u002Fapi\u002Fdata').then(...), 30000)` for auto-refresh\n- **No SPA routing** — single `index.html`, no client-side router needed\n\n## Complex Tier — Project Structure\n\nWhen using FastAPI + Vite\u002FReact, scaffold this structure using the **copy-ready template files** in `references\u002F`:\n\n```\n\u003Ctask>\u002F\n├── Dockerfile           # Copy from references\u002FDockerfile\n├── start.sh             # Copy from references\u002Fstart.sh\n├── server\u002F\n│   ├── main.py          # Copy from references\u002Fserver-main.py, add your API routes\n│   ├── requirements.txt # Copy from references\u002Frequirements.txt, add your deps\n│   ├── routes\u002F          # API route modules (stocks.py, sectors.py)\n│   └── models.py        # Pydantic response models\n├── frontend\u002F\n│   ├── package.json     # Vite + React + chart libraries\n│   ├── vite.config.js   # Copy from references\u002Fvite.config.js\n│   ├── index.html\n│   └── src\u002F\n│       ├── App.jsx      # Main app with routing\u002Ftabs\n│       ├── components\u002F  # Chart, KPI, Table components\n│       ├── hooks\u002F       # useStockData, useSectorData, etc.\n│       └── utils\u002F       # formatters, color helpers\n└── verify.py            # Copy from references\u002Fverification.md (optional)\n```\n\n### Setup Workflow\n\n1. **Copy template files** from `references\u002F` into `\u003Ctask>\u002F` — they work with zero modifications for port 8050\n2. **Add your API routes** to `server\u002Fmain.py` (the template includes CORS, `HEAD \u002F`, `\u002Fhealthz`, and static file serving)\n3. **Add your Python deps** to `server\u002Frequirements.txt` (template includes fastapi + uvicorn)\n4. **Write frontend code** in `frontend\u002Fsrc\u002F` (vite.config.js template proxies `\u002Fapi` to backend on port 8051)\n5. **Serve**: `GetPreviewUrl(port=8050, command=\"bash \u003Ctask>\u002Fstart.sh\", title=\"Dashboard\")`\n\n### Template Files\n\n**`Dockerfile`** ([references\u002FDockerfile](references\u002FDockerfile)) — Python 3.12 + Node + uv + tzdata. Builds frontend at image time, serves static files from FastAPI. Uses `uv pip install` for fast dependency installation.\n\n**`start.sh`** ([references\u002Fstart.sh](references\u002Fstart.sh)) — Cold-boot safe Docker wrapper. Starts dockerd if needed, builds image (uses layer cache on re-runs), removes old container, starts new one, health-checks with log dump on failure. Env var overrides: `PORT` (default 8050), `NAME` (default \"dashboard\").\n\n**`server\u002Fmain.py`** ([references\u002Fserver-main.py](references\u002Fserver-main.py)) — FastAPI skeleton with CORS, `HEAD \u002F` (liveness for platform proxy), `\u002Fhealthz`, SPA catch-all route for client-side routing (`\u002Ftab\u002Fnews`, `\u002Fstocks\u002FAAPL` → `index.html`), and a 503 fallback if the frontend build is missing.\n\n**`server\u002Frequirements.txt`** ([references\u002Frequirements.txt](references\u002Frequirements.txt)) — Minimal: fastapi + uvicorn. Append project-specific packages (pandas, yfinance, etc.).\n\n**`frontend\u002Fvite.config.js`** ([references\u002Fvite.config.js](references\u002Fvite.config.js)) — Vite + React with `\u002Fapi` proxy to backend on port 8051.\n\n> **Critical: Vite proxy is dev-only.** The `proxy` setting in `vite.config.js` only applies during `npm run dev`. The production build outputs plain static files with no proxy. In the Docker image, FastAPI serves both the built SPA from `frontend\u002Fdist\u002F` and all `\u002Fapi\u002F*` routes from the **same port**. The reference `server-main.py` template already does this correctly — do NOT use a two-process architecture with separate static file server and API server.\n\n### Docker Gotchas\n\n- **MCP tools are host-only**: The `tools\u002F` modules (e.g., `from tools.yf_price import ...`) exist only in the host workspace Python environment — they are NOT copied into the Docker image. FastAPI server code inside Docker must call `yfinance` directly. Add `yfinance` to `server\u002Frequirements.txt`\n- **`--network host`**: Required for the container to reach external APIs (yfinance, MCP servers). Already set in `start.sh` template\n- **`tzdata`**: Required for yfinance timezone handling. Already in `Dockerfile` template\n- **Image cache**: Persists across workspace restarts. First build: 30-60s. Subsequent builds: ~2-5s (cached layers)\n- **Logs**: Use `docker logs dashboard` to debug startup failures\n- **Fallback without Docker**: If Docker is unavailable, build the frontend and run FastAPI directly:\n  ```bash\n  fuser -k 8050\u002Ftcp 2>\u002Fdev\u002Fnull || true\n  cd frontend && npm install --prefer-offline && npm run build && cd ..\n  cd server && uvicorn main:app --host 0.0.0.0 --port 8050\n  ```\n\n## Chart Libraries\n\n### Simple Tier (CDN-loaded, no install)\n\n| Library | CDN URL | Best for |\n|---------|---------|----------|\n| **Chart.js** | `https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fchart.js@4\u002Fdist\u002Fchart.umd.min.js` | Line, bar, pie, doughnut, area |\n| **Plotly.js** | `https:\u002F\u002Fcdn.plot.ly\u002Fplotly-2.35.2.min.js` | Candlestick, heatmap, treemap |\n| **Lightweight Charts** | `https:\u002F\u002Funpkg.com\u002Flightweight-charts@4\u002Fdist\u002Flightweight-charts.standalone.production.js` | TradingView-style candlestick |\n\nDefault to **Chart.js**. Use Plotly for candlesticks\u002Fheatmaps. Lightweight Charts only for TradingView-style.\n\n### Complex Tier (npm packages)\n\n| Library | Package | Best for |\n|---------|---------|----------|\n| **Recharts** | `recharts` | Composable React charts — line, bar, area, pie |\n| **Plotly React** | `react-plotly.js plotly.js` | Candlestick, heatmap, treemap |\n| **Lightweight Charts** | `lightweight-charts` | TradingView-style financial charts |\n\nDefault to **Recharts**. Use Plotly for advanced financial charts.\n\nSee [references\u002Fchart-patterns.md](references\u002Fchart-patterns.md) for ready-to-use code snippets.\n\n## Common Dashboard Patterns\n\n### 1. Single Stock Dashboard\n\n**Data**: `get_stock_history`, `get_company_info`, `get_analyst_price_targets`, `get_news`\n\nLayout:\n- KPI row: current price, day change %, 52-week range, market cap, P\u002FE\n- Price chart (line\u002Fcandlestick) with volume bars\n- Analyst price target range (horizontal bar)\n- Recent news list\n\n### 2. Multi-Stock Comparison\n\n**Data**: `get_multiple_stocks_history`, `compare_valuations`, `compare_financials`\n\nLayout:\n- Normalized price overlay chart (base 100)\n- Performance bar chart (YTD, 1Y, 3Y returns)\n- Valuation comparison table (P\u002FE, EV\u002FEBITDA, P\u002FB, etc.)\n- Revenue\u002Fearnings growth comparison\n\n### 3. Sector Heatmap\n\n**Data**: `get_sector_info`, `screen_stocks` with sector filters, `get_predefined_screen`\n\nLayout:\n- Treemap colored by daily\u002Fweekly performance\n- Sector summary cards (top movers, average P\u002FE)\n- Top gainers\u002Flosers table\n- Sector rotation chart\n\n### 4. Earnings Tracker\n\n**Data**: `get_earnings_calendar`, `get_earnings_data`, `get_earnings_estimates`\n\nLayout:\n- Calendar view with upcoming earnings dates\n- Beat\u002Fmiss history chart (bar chart with surprise %)\n- EPS estimate vs actual trend line\n- Revenue estimate revision chart\n\n### 5. Portfolio Monitor\n\n**Data**: the holdings, their live prices and sectors through the kit (`portfolio.holdings`, `la.stream`, `profiles.get`); `compare_valuations` and history from the backend for what the kit does not have\n\nLayout:\n- Holdings table (ticker, shares, price, value, weight, day P&L)\n- Allocation pie chart (by sector\u002Fstock)\n- Total portfolio value line chart\n- Sector exposure bar chart\n\n## Best Practices\n\n### General\n\n- **Data-first**: Fetch and validate the data the backend serves before writing any HTML\u002FReact code (the kit's data comes live in the page)\n- **Fail gracefully**: If a ticker is invalid or API returns empty, show \"No data available\" — don't crash\n- **No console errors**: Verify chart rendering works before calling `GetPreviewUrl`\n- **Responsive**: CSS Grid `auto-fit` for layouts. No horizontal scroll at any width\n- **Performance**: Resample data if > 1000 rows. Don't load unused chart libraries\n\n### Simple Tier\n\n- **Embed data as JSON**: `\u003Cscript>const DATA = ${json.dumps(data)}\u003C\u002Fscript>` — never inline raw Python dicts\n- **Escape properly**: Always use `json.dumps()` with `ensure_ascii=False` for safe JSON embedding\n- **Self-contained**: All CSS in `\u003Cstyle>`, all JS in `\u003Cscript>`, libraries via CDN `\u003Cscript src=\"...\">`\n- **One HTML file**: Keep everything in a single `index.html` — eliminates path bugs\n\n### Complex Tier\n\n- **Separation of concerns**: FastAPI = data API, Vite\u002FReact = UI rendering\n- **Pydantic models**: Define response schemas for type safety\n- **Component per widget**: One React component per chart\u002Fcard\u002Ftable\n- **Shared hooks**: `useStockData(ticker)`, `useSectorData(key)` for data fetching\n- **Error boundaries**: Wrap chart components so one failure doesn't crash the whole page\n- **Single-port production**: Vite proxy is dev-only. In production Docker builds, FastAPI serves both `\u002Fapi\u002F*` and the SPA from one port\n- **`host: '0.0.0.0'`**: Both FastAPI and Vite must bind to `0.0.0.0`, not `127.0.0.1` or `localhost`\n\n## Error Handling & Debugging\n\n| Problem | Solution |\n|---------|----------|\n| `GetPreviewUrl` returns error | Port already in use — try a different port (8051, 8052, ...) |\n| Page is blank | Check for JS errors — ensure all `getElementById` targets exist |\n| Data is empty | Validate MCP tool response before embedding — check for `None` or empty lists |\n| Buttons\u002Finputs do nothing | CSP blocking inline handlers — replace `onclick=` etc. with `addEventListener`. Run CSP self-check |\n| FastAPI won't start | Ensure `host='0.0.0.0'` in `uvicorn.run()` |\n| Vite won't start | Ensure `--host 0.0.0.0` flag and check if port is free |\n| CORS errors | Add `CORSMiddleware` to FastAPI or use Vite proxy |\n| Charts don't render | CDN scripts must load before chart initialization — use `DOMContentLoaded` event |\n| Iframe shows \"refused to connect\" | Server not ready yet — add a small delay or retry logic |\n| HEAD \u002F returns 404 or 405 | Add `@app.head(\"\u002F\")` as its own function — don't stack with `\u002Fhealthz` (use `server-main.py` template) |\n| SPA deep route returns 404 | Add catch-all `@app.get(\"\u002F{full_path:path}\")` that serves `index.html` for non-file paths (use `server-main.py` template) |\n| `start.sh` fails on restart | Ensure idempotent: dockerd startup check, `docker rm -f` before `docker run` (use `start.sh` template) |\n| Docker: yfinance timezone error | Add `tzdata` package to Dockerfile (included in template) |\n| Docker: can't reach external APIs | Use `--network host` flag (included in `start.sh` template) |\n| `GetPreviewUrl` not found \u002F `NameError` | Tool only available to main agent runtime — sub-agents start the server from Bash with `&` and its output sent to a file, then report port\u002Fcommand\u002Ftitle back |\n| Playwright `ERR_CONNECTION_REFUSED` | Use `127.0.0.1:PORT` not `localhost` — sandbox resolves `localhost` to IPv6 (`::1`) first |\n| Background server died \u002F `\u003Cdefunct>` | Start the server from Bash with `&` and its output sent to a file — do NOT use `subprocess.Popen` from `execute_code` (process becomes zombie when tool-call shell exits) |\n| `ModuleNotFoundError: tools.*` in Docker | MCP tools are host-only — use `yfinance` directly inside Docker containers |\n\n## Quality Checklist\n\nBefore calling `GetPreviewUrl`:\n\n**Data & Code**\n- [ ] All data fetched and validated (no empty dataframes or None values)\n- [ ] Files written to `\u003Ctask>\u002F` directory\n- [ ] JSON data properly escaped with `json.dumps()`\n- [ ] All chart containers exist in HTML before JS tries to reference them\n\n**CSP Safety**\n- [ ] No inline event handlers (`onclick`, `oninput`, `onchange`, etc.) — all events via `addEventListener`\n- [ ] No `eval()`, `new Function()`, or string-based `setTimeout()`\n- [ ] No `javascript:` URLs\n- [ ] CSP self-check grep passes (no matches)\n\n**Verification**\n- [ ] Tier 1: JS syntax check passed (`node --check` on extracted script blocks)\n- [ ] Tier 2: Playwright verification passed (for interactive dashboards with buttons\u002Ffilters\u002Ftabs)\n\n**Serving**\n- [ ] Server binds to `0.0.0.0` (not `127.0.0.1` or `localhost`)\n- [ ] Correct port used (default 8050)\n- [ ] Command passed to `GetPreviewUrl` is idempotent (works on re-run after restart)\n- [ ] Complex tier: `start.sh` and `Dockerfile` copied from templates\n- [ ] Complex tier: FastAPI includes `HEAD \u002F` endpoint (use `server-main.py` template)\n\n**UI Quality**\n- [ ] Theme foundation block included verbatim; only bridge variables (and `pick()` for canvas colors) used — no palette hexes in styles\n- [ ] Renders correctly in **both** light and dark\n- [ ] Responsive layout — no horizontal scroll\n- [ ] Financial numbers properly formatted (currency, %, abbreviations)\n- [ ] Title passed to `GetPreviewUrl` is descriptive (e.g., \"AAPL Stock Dashboard\", not \"Preview\")\n",{"name":5,"description":7},"main","ea19577392ca501f413a64cda852ad3fa582c499","plugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard",387,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha","https:\u002F\u002Flangalpha.ai","https:\u002F\u002Fgithub.com\u002Fginlix-ai","2026-11-14 01:05:14",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add ginlix-ai\u002FLangAlpha --skill interactive-dashboard",{"tool":63,"command":64},"bunx","bunx skills add ginlix-ai\u002FLangAlpha --skill interactive-dashboard",{"tool":66,"command":67},"pnpm","pnpm dlx skills add ginlix-ai\u002FLangAlpha --skill interactive-dashboard","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002FSKILL.md",true,[71,77,81,85,89,95,99,103,107,111],{"path":72,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":75,"sourceHtmlUrl":76},"references\u002Fchart-patterns.md","REFERENCE","参考文档",57222,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fchart-patterns.md",{"path":78,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":79,"sourceHtmlUrl":80},"references\u002FDockerfile",770,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002FDockerfile",{"path":82,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":83,"sourceHtmlUrl":84},"references\u002FDockerfile.fastapi-html",408,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002FDockerfile.fastapi-html",{"path":86,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":87,"sourceHtmlUrl":88},"references\u002Frequirements.txt",26,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Frequirements.txt",{"path":90,"resourceType":91,"resourceTypeDesc":92,"sizeBytes":93,"sourceHtmlUrl":94},"references\u002Fserver-main.fastapi-html.py","SCRIPT","脚本",716,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fserver-main.fastapi-html.py",{"path":96,"resourceType":91,"resourceTypeDesc":92,"sizeBytes":97,"sourceHtmlUrl":98},"references\u002Fserver-main.py",1297,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fserver-main.py",{"path":100,"resourceType":91,"resourceTypeDesc":92,"sizeBytes":101,"sourceHtmlUrl":102},"references\u002Fstart.sh",1001,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fstart.sh",{"path":104,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":105,"sourceHtmlUrl":106},"references\u002Fui-components.md",32612,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fui-components.md",{"path":108,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":109,"sourceHtmlUrl":110},"references\u002Fverification.md",3829,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fverification.md",{"path":112,"resourceType":91,"resourceTypeDesc":92,"sizeBytes":113,"sourceHtmlUrl":114},"references\u002Fvite.config.js",285,"https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_deliverables\u002Fskills\u002Finteractive-dashboard\u002Freferences\u002Fvite.config.js",[116,129,138,146,156,170],{"skillNo":117,"name":118,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":123,"publishedAt":44},"01M6CCP2R7FYQ1DHW1D59NR7CR","fx-notes","Fixture skill fx-notes","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fplugins\u002Fvalid-full\u002Fskills\u002Ffx-notes\u002FSKILL.md",[122],{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[124,125,126,127,128],"fx","notes","fx notes","fixture skill","fixture",{"skillNo":130,"name":131,"nameZh":6,"description":132,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":133,"categories":134,"keywords":136,"publishedAt":44},"01M6CCP16MF4A5J06320JAV03Z","good","Fixture skill good","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fplugins\u002Fskill-defects\u002Fskills\u002Fgood\u002FSKILL.md",[135],{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[131,137,127,128],"skill good",{"skillNo":139,"name":140,"nameZh":6,"description":141,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":142,"categories":143,"keywords":145,"publishedAt":44},"01M6CCNZAYQT0RT73XFTFPTJC0","survivor","Fixture skill survivor","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fplugins\u002Fmcp-doc-invalid\u002Fskills\u002Fsurvivor\u002FSKILL.md",[144],{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[140,128,127],{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":44},"01M6CCNX73ZH4DY2W1K0CH5BNP","alpha-notes","Notes on alpha things.","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fplugins\u002Fmarketplace-mixed\u002Fplugins\u002Falpha\u002Fskills\u002Falpha-notes\u002FSKILL.md",[],[125,153,154,155],"alpha","alpha notes","things",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":44},"01M6CCNVK5ZV4CZ2T0M6SH8QB9","cli-tips","Tips for the command line.","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fplugins\u002Fdialect-cursor\u002Fskills\u002Fcli-tips\u002FSKILL.md",[],[163,164,165,166,167,168,169],"tips","cli tips","cli","for","command","line","command line",{"skillNo":171,"name":172,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":174,"categories":175,"keywords":177,"publishedAt":44},"01M6CCNSA684PA7S3NSET4022S","run-workflow","Orchestrate parallel subagent pipelines from a JavaScript workflow script. Fan out work across many items (tickers, filings, findings) then synthesize, or run a saved workflow by name. Unlocks the RunWorkflow tool.","https:\u002F\u002Fgithub.com\u002Fginlix-ai\u002FLangAlpha\u002Fblob\u002Fmain\u002Fplugins\u002Flangalpha_service\u002Fskills\u002Frun-workflow\u002FSKILL.md",[176],{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195],"workflow","run workflow","run","across","javascript","orchestrate","subagent","tool","parallel","work","pipelines","synthesize","name","filings","script","saved","fan out","by name"]