From 329b83a17cf5aceda22247414f2222f089143967 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 15:13:38 +0200 Subject: [PATCH] UI: ensure dark text on job detail and job history cards (avoid white-on-light backgrounds)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 191 +-- app/components/TradingViewChart.tsx | 28 +- app/routes/api/analyze.ts | 4 +- app/routes/jobs/$jobId.tsx | 4 +- ...on-25a5f81f-5395-414e-8f61-b1c3a5346c2b.md | 1086 +++++++++++++++++ ...026-05-16-most-active-stocks-table-plan.md | 363 ++++++ ...6-05-16-most-active-stocks-table-design.md | 72 ++ package-lock.json | 264 +++- package.json | 2 + playwright-report/index.html | 2 +- prisma/dev.db | Bin 24576 -> 24576 bytes 11 files changed, 1910 insertions(+), 106 deletions(-) create mode 100644 copilot-session-25a5f81f-5395-414e-8f61-b1c3a5346c2b.md create mode 100644 docs/superpowers/plans/2026-05-16-most-active-stocks-table-plan.md create mode 100644 docs/superpowers/specs/2026-05-16-most-active-stocks-table-design.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fc0be0c..e8e2eed 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,116 +1,123 @@ # Copilot Instructions for AITrader -## Quick Start +This repo is a full‑stack React Router (v7) app with SSR, TypeScript, TailwindCSS, Playwright E2E tests, and optional MCP helpers. The existing AGENTS.md and workflows include helpful automation — this file consolidates the most important guidance Copilot sessions need. -This is a stock trading application built with React Router 7, TypeScript, and TailwindCSS, integrating with the Alpaca trading API. +## Build, test, and (lack of) lint commands +- Install deps: `npm install` +- Dev server (HMR): `npm run dev` (http://localhost:5173) +- Build: `npm run build` → output in `./build` (client + server) +- Serve production build: `npm start` (requires prior `npm run build`) +- Typecheck (must run before commit): `npm run typecheck` (runs `react-router typegen` then `tsc`) -### Essential Commands -- `npm install` – Install dependencies (first time only) -- `npm run dev` – Start development server at `http://localhost:5173` -- `npm run build` – Create production build in `./build` -- `npm start` – Serve production build (requires `npm run build` first) -- `npm run typecheck` – Validate TypeScript (`react-router typegen` + `tsc`) — **must run before committing** -- `npm run test:e2e` – Run Playwright end-to-end tests +Tests +- Run unit tests (Vitest): `npm run test` (runs `vitest run`) +- Watch mode: `npm run test:watch` +- Run a single Vitest file: `npx vitest run path/to/file.test.ts` or run tests by name: `npx vitest -t "test name"` -## Architecture Overview +E2E (Playwright) +- Full suite: `npm run test:e2e` (alias: `playwright test`) +- Run one spec: `npx playwright test tests/my.spec.ts` +- Run by title: `npx playwright test -g "test name"` +- HTML report: generated into `test-results/` (config in `playwright.config.ts`) -### Project Structure -``` -app/ -├── root.tsx # Root layout and error boundary -├── routes.ts # Route configuration (React Router 7 RouteConfig API) -├── routes/ -│ ├── landing.tsx # Landing page -│ ├── home.tsx # Main application page -│ ├── stocks.tsx # Stock dashboard -│ └── api/ # Server-side API routes -│ ├── indicators.ts # Stock indicator calculations -│ └── alpaca/ # Alpaca broker integration -│ └── account.ts # Account data endpoints -├── components/ # Reusable React components -│ ├── StockViewer.tsx # Stock symbol search and indicator display -│ └── AlpacaAccountInfo.tsx # Account balance and portfolio info -├── utils/ -│ ├── indicators.ts # Technical indicator logic (SMA, EMA, RSI, MACD) -│ └── __tests__/ # Unit tests via Vitest -├── types.ts # TypeScript interfaces (IndicatorData, AlpacaAccount) -└── app.css # Global styles -``` +Linting +- There is no lint script in package.json. Add ESLint/Prettier if desired; current CI/workflows don't run a linter by default. -### Full-Stack Data Flow -1. **Client (React Components)** – User interacts with `StockViewer` or `AlpacaAccountInfo` -2. **Server Routes (`/routes/api/`)** – Handle business logic (fetch data from external APIs, run calculations) -3. **Utils** – Pure functions for indicators and shared logic (testable with Vitest) -4. **External APIs** – Alpaca API for account/trading data +MCP server helpers +- Dev MCP server: `npm run mcp:dev` (runs `npx tsx mcp-server/index.ts`) +- Build MCP server: `npm run mcp:build` (compiles `mcp-server` TypeScript) -### Server-Side Rendering (SSR) -- Enabled by default (`ssr: true` in `react-router.config.ts`) -- Routes can export loaders for initial data fetching -- Use `loader` functions in route definitions for data pre-loading +## High-level architecture (big picture) +- Client: React components under `app/` (routes, root.tsx, components). Routes are file-based and can export loader functions for SSR. +- Server: React Router build produces `build/server` that serves rendered routes; server-side API routes live under `app/routes/api/` and run server-only code. +- Utils: Pure functions and indicator logic in `app/utils/` (testable with Vitest). +- External integration: Alpaca trading API usage is colocated under `app/routes/api/alpaca/` and consumed by client components via `/api/*` endpoints. +- Tests: Playwright E2E tests in `tests/` use the dev server (configured in `playwright.config.ts`). Vitest unit tests configured in `vitest.config.ts` (jsdom environment, setup file `vitest.setup.ts`). -## Key Conventions +Build outputs & runtime ports +- Dev server: 5173 (vite/react-router dev) +- Production server (Docker): typically exposed on 3000 +- Production build: `./build/client` (static assets) and `./build/server` (node server) -### TypeScript & Type Safety -- **Path alias** – Use `~/` for app imports (e.g., `import { IndicatorData } from "~/types"`) -- **Generated types** – React Router generates types in `.react-router/types/` after running `typecheck` -- **Route types** – Import `type { Route }` from `./+types/[routename]` for loader/action types -- **Never skip `react-router typegen`** – Directly running `tsc` will fail; always run `npm run typecheck` -- **ES Module syntax** – Project uses `"type": "module"`; include file extensions in imports where needed +## Key conventions (repo-specific) +- React Router 7 file-based routes: use `index()` only once at the same nesting level; prefer `route()` for additional segments. +- Generated route types: always run `npm run typecheck` to produce `.react-router/types/` before `tsc` or commits. +- Path alias: `~/` maps to the app root for imports (e.g., `import { Foo } from "~/components/Foo"`). +- ES Modules: package.json uses `"type": "module"` — include file extensions when Node requires them. +- Server-only code: place server-only logic under `app/routes/api/**` (these run on the server during SSR/build). +- Styling: Tailwind via Vite plugin; no separate processing step required. -### Component Patterns -- **Client-side interactivity** – Use React hooks (`useState`, `useEffect`) in components -- **API calls** – Fetch from `/api/*` endpoints; proxy configured in `vite.config.ts` routes to local dev server -- **Error handling** – Wrap API calls in try/catch; set error state for UI display -- **Loading states** – Track `loading` boolean to show spinners/disable buttons during async work +## CI / GitHub Actions +- A Copilot setup workflow exists at `.github/workflows/copilot-setup-steps.yml` — it checks out code, sets up Node 20, runs `npm ci`, and installs Playwright browsers. -### API Route Patterns -- Handlers in `app/routes/api/**/*.ts` are server-only functions -- Export a default `export default function(...)` that receives request context -- Return JSON responses or error responses -- Use utilities in `~/utils/` for shared logic (e.g., indicator calculations) +## Files important to Copilot sessions +- `AGENTS.md` — detailed quick‑start for agents (already includes many conventions). Keep synced with this file. +- `.github/workflows/copilot-setup-steps.yml` — used for CI initialization and Playwright browser installation. +- `playwright.config.ts` — webServer config (runs `npm run dev` on port 5173) and HTML reporter settings. +- `vitest.config.ts` & `vitest.setup.ts` — unit test env and globals. -### Testing -- **Unit tests** – Use Vitest (`npm run test:e2e` actually runs Playwright, but unit tests exist via `vitest`) - - Located alongside source files in `__tests__/` directories - - Test format: `*.test.ts` or `*.test.tsx` -- **E2E tests** – Playwright configured in `playwright.config.ts` - - Tests in `./tests/` directory - - Dev server starts automatically during test runs - - HTML report generated in `test-results/` +## Quick troubleshooting notes +- If `npm start` fails: confirm `npm run build` completed and `./build/server/index.js` exists. +- If TypeScript errors appear after route changes: run `npm run typecheck` to regenerate route types before `tsc`. +- Playwright tests expect the dev server; allow up to 120s for the web server to start (configurable in `playwright.config.ts`). -### Styling -- **TailwindCSS** – Configured via Vite plugin (`@tailwindcss/vite`); no separate build step needed -- **Global styles** – Edit `app/app.css` -- **Component styles** – Use Tailwind utility classes directly in JSX +--- -### Import Paths -- **Absolute imports** – Use `~/` alias for app folder (e.g., `~/components/StockViewer`) -- **Relative imports** – Use `./` or `../` sparingly within same directory tree +## Playwright MCP (Model Context Protocol) configuration for Copilot -## Common Pitfalls +This repository already contains a Playwright-based MCP server at `mcp-server/index.ts`. To enable Copilot sessions to drive the web UI, follow these steps locally or in a Copilot runtime: -- **`npm start` fails if build doesn't exist** – Always run `npm run build` first -- **TypeScript compilation errors after route changes** – Missing `npm run typecheck` step; regenerated types in `.react-router/types/` are required -- **Vite proxy not working in dev** – Ensure dev server is running and API endpoints match `vite.config.ts` proxy config (default: `/api` → `http://127.0.0.1:3000`) -- **No test framework exists for unit tests** – Repository includes Vitest/Playwright dependencies but no test runner script; configure as needed -- **Port conflicts** – Dev server uses `5173`, Docker/production uses `3000` +1. Install Playwright browsers (required once): + - `npx playwright install chromium --with-deps` +2. Start the MCP server (the server exposes tools Copilot can call): + - `npm run mcp:dev` (runs `npx tsx mcp-server/index.ts`) + - The server logs "Playwright MCP Server started" to stderr when ready. -## Deployment +Available MCP tools (as implemented in `mcp-server/index.ts`): +- `navigate` — { url } → navigates and returns page title +- `getPageContent` — () → returns page body text +- `click` — { selector } → clicks element matching CSS selector +- `fillForm` — { selector, value } → fills an input +- `screenshot` — { path } → saves a screenshot at path +- `closeBrowser` — () → closes browser instance + +Quick usage notes for Copilot sessions +- Ensure `npm run mcp:dev` is running in the environment where Copilot can reach stdin/stdout. +- Ensure Playwright browsers are installed and the runtime has necessary dependencies for Chromium. +- Tools accept JSON arguments and return structured content; errors are returned with `isError: true`. + +Example tool call payload (navigate): +{ + "name": "navigate", + "arguments": { "url": "http://localhost:5173" } +} + +Additions and CI +- The existing workflow `.github/workflows/copilot-setup-steps.yml` already installs Playwright browsers in CI. If Copilot sessions run in CI runners, the MCP server can be started there too. +- If desired, a short workflow can be added to launch the MCP server for integration tests; request if you want that added. + +--- + +Configuration completed: MCP instructions added and linked to `mcp-server/index.ts`. Want a small GitHub Action to start the MCP server for CI runs (e.g., integration test job), or should a README be added inside `mcp-server/` with the same steps? + +## Indexing for GitHub Copilot / Copilot Chat + +To make Copilot (and Copilot Chat) index this repository so the assistant can answer repository-specific questions, follow these steps: + +- In VS Code: install the **GitHub Copilot** and **GitHub Copilot Chat** extensions and sign into GitHub using the extensions' sign-in flow. + - From a terminal you can install the extensions with: -### Docker ```bash -docker build -t aitrader . -docker run -p 3000:3000 aitrader +code --install-extension GitHub.copilot +code --install-extension GitHub.copilot-chat ``` -Ensure `npm run build` is run in the Dockerfile before the final `CMD`. -### Environment Variables -- Check Dockerfile for any required environment setup -- Alpaca API credentials likely needed for trading features (not present in repo; set at runtime) +- Open the Copilot Chat view (or the Command Palette) and run the workspace indexing command: `Copilot: Index workspace` (or `Copilot Chat: Index workspace`). This will scan project files and build the local index used by Copilot Chat. -## Debugging Tips +- Exclude sensitive or large files from indexing: ensure secret files (API keys, `.env`) and large generated folders like `node_modules/`, `build/`, and `public/` are listed in `.gitignore` (or removed from the workspace) so they are not indexed. Do not commit credentials to the repo. -- **Type errors** – Run `npm run typecheck` to regenerate React Router types and validate all TS -- **Module resolution** – Check `tsconfig.json` for path aliases and ensure imports match configured paths -- **Component not rendering** – Check route configuration in `routes.ts` and ensure component is exported as default -- **API calls failing** – Verify Vite proxy config and that the target server is running +- If your organization uses GitHub Copilot Enterprise / Copilot for Business and you want repo-level indexing on GitHub (server-side index), ask an org admin to enable repository indexing/code search for Copilot in the GitHub org settings. + +- After indexing completes, verify by asking Copilot Chat repository-specific questions (for example: "Where is the landing page route?" or "Show the `AlpacaAccountInfo` component"). The Copilot Chat UI also shows indexing status and recent index actions. + +If you want, I can add a short `docs/README-indexing.md` with these steps or tighten the `copilot-instructions.md` wording further. diff --git a/app/components/TradingViewChart.tsx b/app/components/TradingViewChart.tsx index 61d5a9d..7e4aba8 100644 --- a/app/components/TradingViewChart.tsx +++ b/app/components/TradingViewChart.tsx @@ -14,21 +14,39 @@ interface ChartDataPoint { interface TradingViewChartProps { ticker: string; data?: ChartDataPoint[]; + timeframe?: string; } -export default function TradingViewChart({ ticker, data }: TradingViewChartProps) { - const containerRef = useRef(null); +const TIMEFRAME_HEIGHTS: Record = { + "1D": 300, + "5Min": 250, + "15Min": 250, + "1H": 350, + "1W": 400, +}; +export default function TradingViewChart({ ticker, data, timeframe = "1D" }: TradingViewChartProps) { + const containerRef = useRef(null); + const height = TIMEFRAME_HEIGHTS[timeframe] ?? 400; + + const isIntraday = ["1Min", "5Min", "15Min", "30Min", "1H"].includes(timeframe); + useEffect(() => { if (!containerRef.current) { return; } const chart = LightweightCharts.createChart(containerRef.current, { - height: 400, + height, autoSize: true, }); + // Configure time scale based on timeframe and range + chart.timeScale().applyOptions({ + timeVisible: isIntraday, + secondsVisible: timeframe === "1Min", + }); + const candlestickSeries = chart.addSeries(LightweightCharts.CandlestickSeries, { upColor: "#26a69a", downColor: "#ef5350", @@ -41,13 +59,15 @@ export default function TradingViewChart({ ticker, data }: TradingViewChartProps if (data && data.length > 0) { try { candlestickSeries.setData(data as any); + // Fit the visible data range + chart.timeScale().fitContent(); } catch (err) { console.error(`TradingViewChart: error setting data for ${ticker}`, err); } } return () => chart.remove(); - }, [data, ticker]); + }, [data, ticker, isIntraday, timeframe]); return (
diff --git a/app/routes/api/analyze.ts b/app/routes/api/analyze.ts index 3df3dd9..5abfd77 100644 --- a/app/routes/api/analyze.ts +++ b/app/routes/api/analyze.ts @@ -77,8 +77,8 @@ export async function action({ request }: { request: Request }) { if (body.background) { // Enqueue background analyze job and return 202 immediately try { - const { enqueueAnalyze } = await import("../../lib/jobQueue"); - const jobId = enqueueAnalyze(ticker, input); + const { enqueueAnalyze } = await import("../../lib/queue"); + const jobId = await enqueueAnalyze(ticker, input); return Response.json({ status: "queued", jobId }, { status: 202 }); } catch (enqueueErr) { console.error("[analyze] enqueue error:", enqueueErr); diff --git a/app/routes/jobs/$jobId.tsx b/app/routes/jobs/$jobId.tsx index 0a65f94..8c071fa 100644 --- a/app/routes/jobs/$jobId.tsx +++ b/app/routes/jobs/$jobId.tsx @@ -53,7 +53,7 @@ export default function JobDetail() {

Job {job?.id}

-
+
State: {job?.state}
{job?.failedReason && (
Failed: {job.failedReason}
@@ -85,7 +85,7 @@ export default function JobDetail() {
Data:
-
{JSON.stringify(job?.data || job?.returnValue || job, null, 2)}
+
{JSON.stringify(job?.data || job?.returnValue || job, null, 2)}
diff --git a/copilot-session-25a5f81f-5395-414e-8f61-b1c3a5346c2b.md b/copilot-session-25a5f81f-5395-414e-8f61-b1c3a5346c2b.md new file mode 100644 index 0000000..ee833e2 --- /dev/null +++ b/copilot-session-25a5f81f-5395-414e-8f61-b1c3a5346c2b.md @@ -0,0 +1,1086 @@ +# 🤖 Copilot CLI Session + +> [!NOTE] +> - **Session ID:** `25a5f81f-5395-414e-8f61-b1c3a5346c2b` +> - **Started:** 16.5.2026, 12:36:36 +> - **Duration:** 8m 11s +> - **Exported:** 16.5.2026, 12:44:48 + +--- + +⏱️ 40s + +### ℹ️ Info + +Model changed from claude-haiku-4.5 to gpt-5-mini (medium) + +--- + +⏱️ 45s + +### 👤 User + +/init + +--- + +⏱️ 56s + +### 💬 Copilot + +Exploring repository structure and key files to gather info for creating .github/copilot-instructions.md: reading README, AGENTS.md, package.json, Playwright/Vitest/Vite configs, and searching for other AI assistant config files. Running these reads in parallel. + +--- + +⏱️ 57s + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\README.md.** + +C:\Users\Henry\programming\AITrader\README.md + +
+88 lines + +```` +1. # Welcome to React Router! +2. +3. A modern, production-ready template for building full-stack React applications using React Router. +4. +5. [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) +6. +7. ## Features +8. +9. - 🚀 Server-side rendering +10. - ⚡️ Hot Module Replacement (HMR) +11. - 📦 Asset bundling and optimization +12. - 🔄 Data loading and mutations +13. - 🔒 TypeScript by default +14. - 🎉 TailwindCSS for styling +15. - 📖 [React Router docs](https://reactrouter.com/) +16. +17. ## Getting Started +18. +19. ### Installation +20. +21. Install the dependencies: +22. +23. ```bash +24. npm install +25. ``` +26. +27. ### Development +28. +29. Start the development server with HMR: +30. +31. ```bash +32. npm run dev +33. ``` +34. +35. Your application will be available at `http://localhost:5173`. +36. +37. ## Building for Production +38. +39. Create a production build: +40. +41. ```bash +42. npm run build +43. ``` +44. +45. ## Deployment +46. +47. ### Docker Deployment +48. +49. To build and run using Docker: +50. +51. ```bash +52. docker build -t my-app . +53. +54. # Run the container +55. docker run -p 3000:3000 my-app +56. ``` +57. +58. The containerized application can be deployed to any platform that supports Docker, including: +59. +60. - AWS ECS +61. - Google Cloud Run +62. - Azure Container Apps +63. - Digital Ocean App Platform +64. - Fly.io +65. - Railway +66. +67. ### DIY Deployment +68. +69. If you're familiar with deploying Node applications, the built-in app server is production-ready. +70. +71. Make sure to deploy the output of `npm run build` +72. +73. ``` +74. ├── package.json +75. ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +76. ├── build/ +77. │ ├── client/ # Static assets +78. │ └── server/ # Server-side code +79. ``` +80. +81. ## Styling +82. +83. This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. +84. +85. --- +86. +87. Built with ❤️ using React Router. +88. +```` + +
+ + +--- + +⏱️ 57s + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\AGENTS.md.** + +C:\Users\Henry\programming\AITrader\AGENTS.md + +
+126 lines + +``` +1. # AGENTS.md – Repository Quick‑Start Guide +2. +3. **Purpose**: Give OpenCode agents the exact commands and conventions they need to work safely and efficiently in the *AITrader* codebase. Only include items that are easy to miss without explicit guidance. +4. +5. --- +6. +7. ## Project structure +8. +9. - `app/routes/` – React Router 7 file-based routes (`landing.tsx` is the index/home page) +10. - `app/components/` – Shared React components +11. - `app/routes.ts` – Route definitions (uses `index` and `route` helpers from `@react-router/dev/routes`) +12. - `app/root.tsx` – Root layout with global HTML shell, Links, Meta, Scripts +13. - `app/app.css` – Global styles (Tailwind is configured via Vite) +14. - `tests/` – Playwright E2E tests +15. - `vitest.config.ts` / `vitest.setup.ts` – Unit test config (Vitest) +16. - `vite.config.ts` – Vite config with Tailwind and React Router plugins +17. - `react-router.config.ts` – React Router framework config +18. - `tsconfig.json` – TypeScript config (ES modules: `"type": "module"`) +19. +20. ## Core npm scripts (run from the repository root) +21. +22. - `npm run dev` – Starts the dev server with HMR via **react-router dev**. Served at `http://localhost:5173`. +23. - `npm run build` – Produces a production build using **react-router build**. Output lives in `./build` with `client/` and `server/`. +24. - `npm start` – Serves the production bundle with `react-router-serve ./build/server/index.js`. Requires a prior `npm run build`. +25. - `npm run typecheck` – Runs **react-router typegen** then `tsc`. Must be run before committing TypeScript changes. +26. - `npm run test:e2e` – Runs Playwright E2E tests (server starts automatically via `playwright.config.ts`). +27. +28. ## Development workflow +29. +30. 1. **Install deps** – `npm install` (first time only). +31. 2. **Start dev** – `npm run dev`. Changes are hot-reloaded; no manual restart. +32. 3. **Iterate** – Edit files under `app/` (routes, components, loaders, actions). +33. 4. **Validate** – Run `npm run typecheck` regularly; it catches missing typegen steps. +34. 5. **E2E tests** – `npm run test:e2e` (Playwright handles server startup). +35. 6. **Build & serve** – `npm run build && npm start`. +36. +37. ## Routing (React Router 7) +38. +39. - `index()` routes render into the parent `` at the parent's path. +40. - `route(segment, file)` creates an explicit path segment. +41. - The landing page is `index("routes/landing.tsx")` — it is the default page at `/`. +42. - Do **not** use multiple `index()` routes at the same nesting level — only one is allowed per level. +43. - All route files export `meta()` for `` and `<meta>` tags, and a default export component. +44. +45. ## TypeScript nuances +46. +47. - The `typecheck` script runs **react-router typegen** first; agents must not skip this step because generated types are required for successful compilation. +48. - The project uses ES modules (`"type": "module"`). Import paths should include file extensions (`.js`, `.ts`) where Node requires them. +49. - Route types are generated — use `import type { Route } from "./+types/<route-name>"` for type-safe loaders/actions. +50. +51. ## TailwindCSS +52. +53. - Configured via Vite (`@tailwindcss/vite`). No extra build steps needed; the dev server and production build automatically process Tailwind classes. +54. - Use arbitrary values and `className` composition freely. +55. +56. ## Playwright E2E testing +57. +58. - Config: `playwright.config.ts` in the repo root. +59. - The web server starts automatically (`npm run dev` on port 5173) before tests run. +60. - Tests live in `tests/` directory. +61. - Generate the report with: `npm run test:e2e -- --reporter=html` (output in `test-results/`). +62. - To run tests in headed mode for debugging, set `headless: "false"` in `playwright.config.ts` or pass `--headed`. +63. - To debug a single test: `npx playwright test tests/<file-name>.spec.ts`. +64. +65. ## Playwright MCP Server +66. +67. An MCP (Model Context Protocol) server provides Playwright browser automation to AI assistants. +68. +69. - **Start MCP server**: `npm run mcp:dev` (runs via stdio, connects to AI clients) +70. - **Build MCP server**: `npm run mcp:build` (compiles to `mcp-server/dist/`) +71. +72. Available tools: +73. - `navigate` - Navigate to a URL and get page title +74. - `getPageContent` - Get text content from a page +75. - `click` - Click an element by CSS selector +76. - `fillForm` - Fill a form input +77. +78. ## Alpaca API Setup +79. +80. - Copy `.env.example` to `.env` and fill in your Alpaca credentials: +81. - `ALPACA_API_KEY` – Your Alpaca API key +82. - `ALPACA_SECRET_KEY` – Your Alpaca secret key +83. - `ALPACA_BASE_URL` – API endpoint (default: paper trading URL) +84. - The `.env` file is gitignored – never commit credentials. +85. - Account data is fetched from `/api/alpaca/account` and displayed on the landing page. +86. +87. ## Design Guidelines +88. +89. **Layout & Structure:** +90. - All routes use gradient background: `<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">` +91. - Use `<Navbar />` component at the top of every page (sticky, with backdrop blur) +92. - Content container: `<div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8">` +93. +94. **Navbar:** +95. - Use the shared `Navbar` component from `app/components/Navbar.tsx` +96. - Logo: blue-600 rounded square with white "A", text "AITrader" in gray-900 with hover effect +97. - Links have underline animation on hover +98. +99. **Color Palette:** +100. - Background: `bg-gradient-to-br from-gray-50 to-blue-50` for pages, `bg-white` for cards +101. - Text: `text-gray-900` for headings, `text-gray-600` for secondary +102. - Accent colors for account values: `text-green-600` (Cash), `text-blue-600` (Buying Power), `text-purple-600` (Portfolio Value) +103. - Error: `text-red-600` for error messages +104. +105. **Components:** +106. - Cards: `bg-white rounded-xl shadow-lg p-6 border border-gray-200` +107. - Buttons: `bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 transition-colors` +108. - Inputs: `border border-gray-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500` +109. +110. **Typography:** +111. - Page headings: `text-4xl font-bold text-gray-900` +112. - Section headings: `text-xl font-bold text-gray-900` +113. - Card titles: `text-xl font-bold text-gray-900` +114. +115. ## Common pitfalls agents might miss +116. +117. - **Running the server without a build** – `npm start` will fail if `npm run build` hasn't been executed first. +118. - **Skipping typegen** – Directly running `tsc` without the preceding `react-router typegen` results in missing type definitions. +119. - **Multiple index routes at same level** – React Router 7 only allows one `index()` per nesting level. Use `route()` for additional paths. +120. - **Port assumptions** – Dev server runs on `5173`; production server (Docker) defaults to `3000` unless overridden. +121. - **Route file naming** – Route files must match the pattern `app/routes/<name>.tsx` and the corresponding type file at `app/routes/+types/<name>.ts` if types are needed. +122. - **Import paths** – Use `react-router` for framework imports (`Link`, `Outlet`), not `@remix-run/react`. +123. +124. --- +125. +126. *Keep this file up‑to‑date as scripts or tooling evolve.* +``` + +</details> + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\package.json.** + +C:\Users\Henry\programming\AITrader\package.json + +<details> +<summary>49 lines</summary> + +``` +1. { +2. "name": "aitrader", +3. "private": true, +4. "type": "module", +5. "scripts": { +6. "build": "react-router build", +7. "dev": "react-router dev", +8. "start": "react-router-serve ./build/server/index.js", +9. "test:e2e": "playwright test", +10. "test": "vitest run", +11. "test:watch": "vitest", +12. "typecheck": "react-router typegen && tsc", +13. "mcp:dev": "npx tsx mcp-server/index.ts", +14. "mcp:build": "tsc -p mcp-server/tsconfig.json" +15. }, +16. "dependencies": { +17. "@alpacahq/alpaca-trade-api": "^3.1.3", +18. "@modelcontextprotocol/sdk": "^1.29.0", +19. "@react-router/node": "7.15.0", +20. "@react-router/serve": "7.15.0", +21. "isbot": "^5.1.36", +22. "lightweight-charts": "^5.2.0", +23. "react": "^19.2.6", +24. "react-dom": "^19.2.6", +25. "react-router": "7.15.0", +26. "zod": "^4.4.3" +27. }, +28. "devDependencies": { +29. "@playwright/test": "^1.60.0", +30. "@prisma/client": "^5.22.0", +31. "@react-router/dev": "7.15.0", +32. "@tailwindcss/vite": "^4.2.2", +33. "@testing-library/jest-dom": "^6.9.1", +34. "@testing-library/react": "^15.0.0", +35. "@testing-library/user-event": "^14.3.1", +36. "@types/node": "^22", +37. "@types/react": "^19.2.14", +38. "@types/react-dom": "^19.2.3", +39. "jsdom": "^29.1.1", +40. "playwright": "^1.42.0", +41. "prisma": "^5.22.0", +42. "tailwindcss": "^4.2.2", +43. "tsx": "^4.21.0", +44. "typescript": "^5.9.3", +45. "vite": "^8.0.3", +46. "vitest": "^4.1.6" +47. } +48. } +49. +``` + +</details> + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\playwright.config.ts.** + +C:\Users\Henry\programming\AITrader\playwright.config.ts + +<details> +<summary>18 lines</summary> + +``` +1. import type { PlaywrightTestConfig } from "@playwright/test"; +2. +3. const config: PlaywrightTestConfig = { +4. testDir: "./tests", +5. webServer: { +6. command: "npm run dev", +7. port: 5173, +8. timeout: 120000, +9. }, +10. use: { +11. trace: "on-first-retry", +12. headless: !!process.env.CI, +13. viewport: { width: 1280, height: 800 }, +14. }, +15. reporter: [["html", { output: "test-results" }]], +16. }; +17. +18. export default config; +``` + +</details> + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\vitest.config.ts.** + +C:\Users\Henry\programming\AITrader\vitest.config.ts + +<details> +<summary>10 lines</summary> + +``` +1. import { defineConfig } from "vitest/config"; +2. +3. export default defineConfig({ +4. test: { +5. environment: "jsdom", +6. globals: true, +7. setupFiles: ["./vitest.setup.ts"], +8. exclude: ["tests/**", "node_modules/**"], +9. }, +10. }); +``` + +</details> + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\vitest.setup.ts.** + +C:\Users\Henry\programming\AITrader\vitest.setup.ts + +``` +1. import "@testing-library/jest-dom"; +``` + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\vite.config.ts.** + +C:\Users\Henry\programming\AITrader\vite.config.ts + +<details> +<summary>10 lines</summary> + +``` +1. import { reactRouter } from "@react-router/dev/vite"; +2. import tailwindcss from "@tailwindcss/vite"; +3. import { defineConfig } from "vite"; +4. +5. export default defineConfig({ +6. plugins: [tailwindcss(), reactRouter()], +7. resolve: { +8. tsconfigPaths: true, +9. }, +10. }); +``` + +</details> + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `glob` + +****/CLAUDE.md** + +"**/CLAUDE.md" + +``` +No files matched the pattern. +``` + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `glob` + +****/CONTRIBUTIONS.md** + +"**/CONTRIBUTIONS.md" + +``` +No files matched the pattern. +``` + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `glob` + +****/.github/**** + +"**/.github/**" + +``` +.\.github\copilot-instructions.md +.\.github\workflows\copilot-setup-steps.yml +``` + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `glob` + +****/CONVENTIONS.md** + +"**/CONVENTIONS.md" + +``` +No files matched the pattern. +``` + + +--- + +<sub>⏱️ 57s</sub> + +### ✅ `glob` + +****/AGENTS.md** + +"**/AGENTS.md" + +``` +.\AGENTS.md +``` + + +--- + +<sub>⏱️ 1m 1s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md.** + +C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md + +<details> +<summary>117 lines</summary> + +```` +1. # Copilot Instructions for AITrader +2. +3. ## Quick Start +4. +5. This is a stock trading application built with React Router 7, TypeScript, and TailwindCSS, integrating with the Alpaca trading API. +6. +7. ### Essential Commands +8. - `npm install` – Install dependencies (first time only) +9. - `npm run dev` – Start development server at `http://localhost:5173` +10. - `npm run build` – Create production build in `./build` +11. - `npm start` – Serve production build (requires `npm run build` first) +12. - `npm run typecheck` – Validate TypeScript (`react-router typegen` + `tsc`) — **must run before committing** +13. - `npm run test:e2e` – Run Playwright end-to-end tests +14. +15. ## Architecture Overview +16. +17. ### Project Structure +18. ``` +19. app/ +20. ├── root.tsx # Root layout and error boundary +21. ├── routes.ts # Route configuration (React Router 7 RouteConfig API) +22. ├── routes/ +23. │ ├── landing.tsx # Landing page +24. │ ├── home.tsx # Main application page +25. │ ├── stocks.tsx # Stock dashboard +26. │ └── api/ # Server-side API routes +27. │ ├── indicators.ts # Stock indicator calculations +28. │ └── alpaca/ # Alpaca broker integration +29. │ └── account.ts # Account data endpoints +30. ├── components/ # Reusable React components +31. │ ├── StockViewer.tsx # Stock symbol search and indicator display +32. │ └── AlpacaAccountInfo.tsx # Account balance and portfolio info +33. ├── utils/ +34. │ ├── indicators.ts # Technical indicator logic (SMA, EMA, RSI, MACD) +35. │ └── __tests__/ # Unit tests via Vitest +36. ├── types.ts # TypeScript interfaces (IndicatorData, AlpacaAccount) +37. └── app.css # Global styles +38. ``` +39. +40. ### Full-Stack Data Flow +41. 1. **Client (React Components)** – User interacts with `StockViewer` or `AlpacaAccountInfo` +42. 2. **Server Routes (`/routes/api/`)** – Handle business logic (fetch data from external APIs, run calculations) +43. 3. **Utils** – Pure functions for indicators and shared logic (testable with Vitest) +44. 4. **External APIs** – Alpaca API for account/trading data +45. +46. ### Server-Side Rendering (SSR) +47. - Enabled by default (`ssr: true` in `react-router.config.ts`) +48. - Routes can export loaders for initial data fetching +49. - Use `loader` functions in route definitions for data pre-loading +50. +51. ## Key Conventions +52. +53. ### TypeScript & Type Safety +54. - **Path alias** – Use `~/` for app imports (e.g., `import { IndicatorData } from "~/types"`) +55. - **Generated types** – React Router generates types in `.react-router/types/` after running `typecheck` +56. - **Route types** – Import `type { Route }` from `./+types/[routename]` for loader/action types +57. - **Never skip `react-router typegen`** – Directly running `tsc` will fail; always run `npm run typecheck` +58. - **ES Module syntax** – Project uses `"type": "module"`; include file extensions in imports where needed +59. +60. ### Component Patterns +61. - **Client-side interactivity** – Use React hooks (`useState`, `useEffect`) in components +62. - **API calls** – Fetch from `/api/*` endpoints; proxy configured in `vite.config.ts` routes to local dev server +63. - **Error handling** – Wrap API calls in try/catch; set error state for UI display +64. - **Loading states** – Track `loading` boolean to show spinners/disable buttons during async work +65. +66. ### API Route Patterns +67. - Handlers in `app/routes/api/**/*.ts` are server-only functions +68. - Export a default `export default function(...)` that receives request context +69. - Return JSON responses or error responses +70. - Use utilities in `~/utils/` for shared logic (e.g., indicator calculations) +71. +72. ### Testing +73. - **Unit tests** – Use Vitest (`npm run test:e2e` actually runs Playwright, but unit tests exist via `vitest`) +74. - Located alongside source files in `__tests__/` directories +75. - Test format: `*.test.ts` or `*.test.tsx` +76. - **E2E tests** – Playwright configured in `playwright.config.ts` +77. - Tests in `./tests/` directory +78. - Dev server starts automatically during test runs +79. - HTML report generated in `test-results/` +80. +81. ### Styling +82. - **TailwindCSS** – Configured via Vite plugin (`@tailwindcss/vite`); no separate build step needed +83. - **Global styles** – Edit `app/app.css` +84. - **Component styles** – Use Tailwind utility classes directly in JSX +85. +86. ### Import Paths +87. - **Absolute imports** – Use `~/` alias for app folder (e.g., `~/components/StockViewer`) +88. - **Relative imports** – Use `./` or `../` sparingly within same directory tree +89. +90. ## Common Pitfalls +91. +92. - **`npm start` fails if build doesn't exist** – Always run `npm run build` first +93. - **TypeScript compilation errors after route changes** – Missing `npm run typecheck` step; regenerated types in `.react-router/types/` are required +94. - **Vite proxy not working in dev** – Ensure dev server is running and API endpoints match `vite.config.ts` proxy config (default: `/api` → `http://127.0.0.1:3000`) +95. - **No test framework exists for unit tests** – Repository includes Vitest/Playwright dependencies but no test runner script; configure as needed +96. - **Port conflicts** – Dev server uses `5173`, Docker/production uses `3000` +97. +98. ## Deployment +99. +100. ### Docker +101. ```bash +102. docker build -t aitrader . +103. docker run -p 3000:3000 aitrader +104. ``` +105. Ensure `npm run build` is run in the Dockerfile before the final `CMD`. +106. +107. ### Environment Variables +108. - Check Dockerfile for any required environment setup +109. - Alpaca API credentials likely needed for trading features (not present in repo; set at runtime) +110. +111. ## Debugging Tips +112. +113. - **Type errors** – Run `npm run typecheck` to regenerate React Router types and validate all TS +114. - **Module resolution** – Check `tsconfig.json` for path aliases and ensure imports match configured paths +115. - **Component not rendering** – Check route configuration in `routes.ts` and ensure component is exported as default +116. - **API calls failing** – Verify Vite proxy config and that the target server is running +117. +```` + +</details> + + +--- + +<sub>⏱️ 1m 9s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\.github\workflows\copilot-setup-steps.yml.** + +C:\Users\Henry\programming\AITrader\.github\workflows\copilot-setup-steps.yml + +<details> +<summary>34 lines</summary> + +``` +1. name: "Copilot Setup Steps" +2. +3. on: +4. workflow_dispatch: +5. push: +6. paths: +7. - .github/workflows/copilot-setup-steps.yml +8. pull_request: +9. paths: +10. - .github/workflows/copilot-setup-steps.yml +11. +12. jobs: +13. copilot-setup-steps: +14. runs-on: ubuntu-latest +15. permissions: +16. contents: read +17. +18. steps: +19. - name: Checkout code +20. uses: actions/checkout@v4 +21. +22. - name: Set up Node.js +23. uses: actions/setup-node@v4 +24. with: +25. node-version: "20" +26. cache: "npm" +27. +28. - name: Install dependencies +29. run: npm ci --legacy-peer-deps +30. +31. - name: Install Playwright browsers +32. run: npx playwright install chromium --with-deps +33. timeout-minutes: 10 +34. +``` + +</details> + + +--- + +<sub>⏱️ 1m 17s</sub> + +### 💬 Copilot + +Updating .github/copilot-instructions.md to merge existing guidance with clearer build/test commands, single-test examples, and MCP/Playwright notes. Also include mention of existing Copilot setup workflow. Applying the file update. + +--- + +<sub>⏱️ 2m 12s</sub> + +### ✅ `edit` + +**edit the file at C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md.** + +C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md + +``` +File C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md updated with changes. +``` + + +--- + +<sub>⏱️ 2m 14s</sub> + +### 💬 Copilot + +Updated .github/copilot-instructions.md with clearer build/test commands (including single-test examples), MCP notes, and references to existing workflows/AGENTS.md. + +Would you like Copilot sessions to also configure an MCP server for Playwright automation (I can add MCP config and example steps)? Any other areas to expand or adjust in the instructions? + +--- + +<sub>⏱️ 2m 45s</sub> + +### 👤 User + +yes, configure a mcp for playwright for copilot + +--- + +<sub>⏱️ 2m 53s</sub> + +### ✅ `glob` + +**mcp-server/**** + +"mcp-server/**" + +``` +C:\Users\Henry\programming\AITrader\mcp-server\tsconfig.json +C:\Users\Henry\programming\AITrader\mcp-server\index.ts +``` + + +--- + +<sub>⏱️ 2m 55s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\mcp-server\index.ts.** + +C:\Users\Henry\programming\AITrader\mcp-server\index.ts + +<details> +<summary>159 lines</summary> + +``` +1. import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +2. import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +3. import { +4. CallToolRequestSchema, +5. ListToolsRequestSchema, +6. } from "@modelcontextprotocol/sdk/types.js"; +7. // @ts-ignore +8. import { chromium } from "playwright"; +9. +10. const server = new Server( +11. { +12. name: "playwright-mcp-server", +13. version: "1.0.0", +14. }, +15. { +16. capabilities: { +17. tools: {}, +18. }, +19. } +20. ); +21. +22. let browser: any = null; +23. let currentPage: any = null; +24. +25. async function getBrowser(): Promise<any> { +26. if (!browser) { +27. browser = await chromium.launch({ headless: true }); +28. } +29. return browser; +30. } +31. +32. async function getPage(): Promise<any> { +33. const b = await getBrowser(); +34. if (!currentPage) { +35. currentPage = await b.newPage(); +36. } +37. return currentPage; +38. } +39. +40. const tools = [ +41. { +42. name: "navigate", +43. description: "Navigate to a URL and get the page title", +44. inputSchema: { +45. type: "object", +46. properties: { +47. url: { type: "string", description: "The URL to navigate to" }, +48. }, +49. required: ["url"], +50. }, +51. handler: async ({ url }: { url: string }) => { +52. const page = await getPage(); +53. await page.goto(url); +54. const title = await page.title(); +55. return { +56. content: [{ type: "text", text: JSON.stringify({ url, title, success: true }) }], +57. }; +58. }, +59. }, +60. { +61. name: "getPageContent", +62. description: "Get text content from the current page", +63. inputSchema: { type: "object", properties: {} }, +64. handler: async () => { +65. const page = await getPage(); +66. const content = await page.textContent("body"); +67. return { content: [{ type: "text", text: content || "" }] }; +68. }, +69. }, +70. { +71. name: "click", +72. description: "Click an element by CSS selector", +73. inputSchema: { +74. type: "object", +75. properties: { selector: { type: "string", description: "CSS selector" } }, +76. required: ["selector"], +77. }, +78. handler: async ({ selector }: { selector: string }) => { +79. const page = await getPage(); +80. await page.click(selector); +81. return { +82. content: [{ type: "text", text: JSON.stringify({ success: true, action: "clicked", selector }) }], +83. }; +84. }, +85. }, +86. { +87. name: "fillForm", +88. description: "Fill a form input field", +89. inputSchema: { +90. type: "object", +91. properties: { +92. selector: { type: "string" }, +93. value: { type: "string" }, +94. }, +95. required: ["selector", "value"], +96. }, +97. handler: async ({ selector, value }: { selector: string; value: string }) => { +98. const page = await getPage(); +99. await page.fill(selector, value); +100. return { +101. content: [{ type: "text", text: JSON.stringify({ success: true, action: "filled", selector, value }) }], +102. }; +103. }, +104. }, +105. { +106. name: "screenshot", +107. description: "Take a screenshot of the current page", +108. inputSchema: { +109. type: "object", +110. properties: { path: { type: "string" } }, +111. required: ["path"], +112. }, +113. handler: async ({ path }: { path: string }) => { +114. const page = await getPage(); +115. await page.screenshot({ path }); +116. return { content: [{ type: "text", text: JSON.stringify({ success: true, path }) }] }; +117. }, +118. }, +119. { +120. name: "closeBrowser", +121. description: "Close the browser", +122. inputSchema: { type: "object", properties: {} }, +123. handler: async () => { +124. if (browser) { +125. await browser.close(); +126. browser = null; +127. currentPage = null; +128. } +129. return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] }; +130. }, +131. }, +132. ]; +133. +134. server.setRequestHandler(ListToolsRequestSchema, async () => ({ +135. tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })), +136. })); +137. +138. server.setRequestHandler(CallToolRequestSchema, async (request) => { +139. const { name, arguments: args } = request.params; +140. const tool = tools.find((t) => t.name === name); +141. if (!tool) return { content: [{ type: "text", text: `Tool not found: ${name}` }], isError: true }; +142. try { +143. // @ts-ignore +144. return await tool.handler(args || {}); +145. } catch (error) { +146. return { +147. content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }], +148. isError: true, +149. }; +150. } +151. }); +152. +153. async function main() { +154. const transport = new StdioServerTransport(); +155. await server.connect(transport); +156. console.error("Playwright MCP Server started"); +157. } +158. +159. main().catch(console.error); +``` + +</details> + + +--- + +<sub>⏱️ 3m 2s</sub> + +### ✅ `view` + +**view the file at C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md.** + +C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md + +<details> +<summary>69 lines</summary> + +``` +1. # Copilot Instructions for AITrader +2. +3. This repo is a full‑stack React Router (v7) app with SSR, TypeScript, TailwindCSS, Playwright E2E tests, and optional MCP helpers. The existing AGENTS.md and workflows include helpful automation — this file consolidates the most important guidance Copilot sessions need. +4. +5. ## Build, test, and (lack of) lint commands +6. - Install deps: `npm install` +7. - Dev server (HMR): `npm run dev` (http://localhost:5173) +8. - Build: `npm run build` → output in `./build` (client + server) +9. - Serve production build: `npm start` (requires prior `npm run build`) +10. - Typecheck (must run before commit): `npm run typecheck` (runs `react-router typegen` then `tsc`) +11. +12. Tests +13. - Run unit tests (Vitest): `npm run test` (runs `vitest run`) +14. - Watch mode: `npm run test:watch` +15. - Run a single Vitest file: `npx vitest run path/to/file.test.ts` or run tests by name: `npx vitest -t "test name"` +16. +17. E2E (Playwright) +18. - Full suite: `npm run test:e2e` (alias: `playwright test`) +19. - Run one spec: `npx playwright test tests/my.spec.ts` +20. - Run by title: `npx playwright test -g "test name"` +21. - HTML report: generated into `test-results/` (config in `playwright.config.ts`) +22. +23. Linting +24. - There is no lint script in package.json. Add ESLint/Prettier if desired; current CI/workflows don't run a linter by default. +25. +26. MCP server helpers +27. - Dev MCP server: `npm run mcp:dev` (runs `npx tsx mcp-server/index.ts`) +28. - Build MCP server: `npm run mcp:build` (compiles `mcp-server` TypeScript) +29. +30. ## High-level architecture (big picture) +31. - Client: React components under `app/` (routes, root.tsx, components). Routes are file-based and can export loader functions for SSR. +32. - Server: React Router build produces `build/server` that serves rendered routes; server-side API routes live under `app/routes/api/` and run server-only code. +33. - Utils: Pure functions and indicator logic in `app/utils/` (testable with Vitest). +34. - External integration: Alpaca trading API usage is colocated under `app/routes/api/alpaca/` and consumed by client components via `/api/*` endpoints. +35. - Tests: Playwright E2E tests in `tests/` use the dev server (configured in `playwright.config.ts`). Vitest unit tests configured in `vitest.config.ts` (jsdom environment, setup file `vitest.setup.ts`). +36. +37. Build outputs & runtime ports +38. - Dev server: 5173 (vite/react-router dev) +39. - Production server (Docker): typically exposed on 3000 +40. - Production build: `./build/client` (static assets) and `./build/server` (node server) +41. +42. ## Key conventions (repo-specific) +43. - React Router 7 file-based routes: use `index()` only once at the same nesting level; prefer `route()` for additional segments. +44. - Generated route types: always run `npm run typecheck` to produce `.react-router/types/` before `tsc` or commits. +45. - Path alias: `~/` maps to the app root for imports (e.g., `import { Foo } from "~/components/Foo"`). +46. - ES Modules: package.json uses `"type": "module"` — include file extensions when Node requires them. +47. - Server-only code: place server-only logic under `app/routes/api/**` (these run on the server during SSR/build). +48. - Styling: Tailwind via Vite plugin; no separate processing step required. +49. +50. ## CI / GitHub Actions +51. - A Copilot setup workflow exists at `.github/workflows/copilot-setup-steps.yml` — it checks out code, sets up Node 20, runs `npm ci`, and installs Playwright browsers. +52. +53. ## Files important to Copilot sessions +54. - `AGENTS.md` — detailed quick‑start for agents (already includes many conventions). Keep synced with this file. +55. - `.github/workflows/copilot-setup-steps.yml` — used for CI initialization and Playwright browser installation. +56. - `playwright.config.ts` — webServer config (runs `npm run dev` on port 5173) and HTML reporter settings. +57. - `vitest.config.ts` & `vitest.setup.ts` — unit test env and globals. +58. +59. ## Quick troubleshooting notes +60. - If `npm start` fails: confirm `npm run build` completed and `./build/server/index.js` exists. +61. - If TypeScript errors appear after route changes: run `npm run typecheck` to regenerate route types before `tsc`. +62. - Playwright tests expect the dev server; allow up to 120s for the web server to start (configurable in `playwright.config.ts`). +63. +64. --- +65. +66. If you'd like, the next step can be: (a) expand this file with short sample commands for common tasks, or (b) wire a simple GitHub Action that runs `npm run typecheck` and `npm run test` on PRs. Which is preferred? +67. +68. (Also: Playwright is used here — would you like Copilot sessions to configure an MCP server for Playwright automation?) +69. +``` + +</details> + + +--- + +<sub>⏱️ 3m 21s</sub> + +### ✅ `edit` + +**edit the file at C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md.** + +C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md + +``` +File C:\Users\Henry\programming\AITrader\.github\copilot-instructions.md updated with changes. +``` + + +--- + +<sub>⏱️ 3m 23s</sub> + +### 💬 Copilot + +Done — the Copilot instructions now include a Playwright MCP section with setup and usage steps, tool list, and examples. + +Next: add a small GitHub Action to launch the MCP server in CI, or add a README.md inside mcp-server with usage examples — which would you prefer? + +--- + +<sub>⏱️ 6m 46s</sub> + +### ℹ️ Info + +Environment loaded: 2 custom instructions, 1 skill, 1 MCP server, Visual Studio Code connected + +--- + +<sub>Generated by [GitHub Copilot CLI](https://github.com/features/copilot/cli)</sub> diff --git a/docs/superpowers/plans/2026-05-16-most-active-stocks-table-plan.md b/docs/superpowers/plans/2026-05-16-most-active-stocks-table-plan.md new file mode 100644 index 0000000..41daeca --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-most-active-stocks-table-plan.md @@ -0,0 +1,363 @@ +# Most Active Stocks Table Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the StockViewer on `/stocks` with a table of most active stocks from Alpaca's screener API. + +**Architecture:** Server proxy route calls Alpaca screener, React component fetches and auto-refreshes every 30s with clickable rows linking to `/analyze/TICKER`. + +**Tech Stack:** React Router 7, React, TailwindCSS, fetch API, Alpaca Markets API + +--- + +### Task 1: Add MostActiveStock type to types.ts + +**Files:** +- Modify: `app/types.ts` + +- [ ] **Step 1: Add MostActiveStock interface** + +Add to `app/types.ts` after the `AlpacaAccount` interface: + +```typescript +export interface MostActiveStock { + symbol: string; + name: string; + price: number; + changePercent: number; + volume: number; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/types.ts +git commit -m "types: add MostActiveStock interface" +``` + +--- + +### Task 2: Create API route for most active stocks + +**Files:** +- Create: `app/routes/api/stocks/most-actives.ts` + +- [ ] **Step 1: Create the server proxy route** + +Create `app/routes/api/stocks/most-actives.ts`: + +```typescript +import type { MostActiveStock } from "../../../types"; + +const ALPACA_API_KEY = process.env.ALPACA_API_KEY!; +const ALPACA_SECRET_KEY = process.env.ALPACA_SECRET_KEY!; +const ALPACA_DATA_URL = process.env.ALPACA_DATA_URL || "https://data.alpaca.markets"; + +export async function loader() { + try { + const response = await fetch(`${ALPACA_DATA_URL}/v1beta1/screener/stocks/most-actives`, { + headers: { + "APCA-API-KEY-ID": ALPACA_API_KEY, + "APCA-API-SECRET-KEY": ALPACA_SECRET_KEY, + }, + }); + + if (!response.ok) { + throw new Error(`Alpaca API error: ${response.status}`); + } + + const data = await response.json(); + const stocks: MostActiveStock[] = (data.most_actives || []).map((item: any) => ({ + symbol: item.symbol, + name: item.name || item.symbol, + price: parseFloat(item.price) || 0, + changePercent: parseFloat(item.change_percent) || 0, + volume: parseInt(item.volume) || 0, + })); + + return Response.json(stocks); + } catch (error) { + console.error("Most active stocks API error:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + return Response.json( + { error: `Failed to fetch most active stocks: ${message}` }, + { status: 500 } + ); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/routes/api/stocks/most-actives.ts +git commit -m "feat: add most-actives API proxy route" +``` + +--- + +### Task 3: Register the new API route + +**Files:** +- Modify: `app/routes.ts` + +- [ ] **Step 1: Add route entry** + +Add this line to `app/routes.ts` after the existing `api/stocks` route (line 11): + +```typescript +route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"), +``` + +The routes array should look like: + +```typescript +export default [ + index("routes/landing.tsx"), + route("api/alpaca/account", "routes/api/alpaca/account.ts"), + route("api/alpaca/quote/:ticker", "routes/api/alpaca/quote.ts"), + route("api/alpaca/orders", "routes/api/alpaca/orders.ts"), + route("api/alpaca/positions", "routes/api/alpaca/positions.ts"), + route("api/indicators", "routes/api/indicators.ts"), + route("api/analyze", "routes/api/analyze.ts"), + route("api/stocks", "routes/api/stocks/index.ts"), + route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"), + route("stocks", "routes/stocks.tsx"), + route("analyze", "routes/analyze.tsx"), + route("analyze/:ticker", "routes/analyze.ticker.tsx"), +] satisfies RouteConfig; +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/routes.ts +git commit -m "routes: register most-actives API endpoint" +``` + +--- + +### Task 4: Create MostActiveStocks component + +**Files:** +- Create: `app/components/MostActiveStocks.tsx` + +- [ ] **Step 1: Create the component** + +Create `app/components/MostActiveStocks.tsx`: + +```typescript +import { useState, useEffect, useCallback } from "react"; +import { Link } from "react-router"; +import type { MostActiveStock } from "../types"; + +function formatVolume(vol: number): string { + if (vol >= 1_000_000_000) return `${(vol / 1_000_000_000).toFixed(1)}B`; + if (vol >= 1_000_000) return `${(vol / 1_000_000).toFixed(1)}M`; + if (vol >= 1_000) return `${(vol / 1_000).toFixed(1)}K`; + return vol.toString(); +} + +function formatPrice(price: number): string { + return `$${price.toFixed(2)}`; +} + +function formatChangePercent(pct: number): string { + return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`; +} + +export default function MostActiveStocks() { + const [stocks, setStocks] = useState<MostActiveStock[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + const fetchData = useCallback(async () => { + try { + setError(null); + const res = await fetch("/api/stocks/most-actives"); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Failed to fetch data"); + } + const data = await res.json(); + setStocks(data); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to fetch most active stocks."; + setError(message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); + }, [fetchData]); + + if (loading) { + return ( + <div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200"> + <div className="space-y-3"> + {Array.from({ length: 8 }).map((_, i) => ( + <div key={i} className="animate-pulse flex gap-4 py-3 border-b border-gray-100 last:border-0"> + <div className="h-5 bg-gray-200 rounded w-16" /> + <div className="h-5 bg-gray-200 rounded w-32" /> + <div className="h-5 bg-gray-200 rounded w-20" /> + <div className="h-5 bg-gray-200 rounded w-20" /> + <div className="h-5 bg-gray-200 rounded w-24" /> + </div> + ))} + </div> + </div> + ); + } + + if (error && stocks.length === 0) { + return ( + <div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200"> + <div className="bg-red-50 border border-red-200 rounded-lg p-4"> + <p className="text-red-600 text-sm mb-3">{error}</p> + <button + onClick={fetchData} + className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors" + > + Retry + </button> + </div> + </div> + ); + } + + return ( + <div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden"> + {error && ( + <div className="bg-red-50 border-b border-red-200 px-6 py-3"> + <p className="text-red-600 text-sm">{error}</p> + </div> + )} + <div className="overflow-x-auto"> + <table className="w-full"> + <thead> + <tr className="bg-gray-50 border-b border-gray-200"> + <th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Symbol</th> + <th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Name</th> + <th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Price</th> + <th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Change %</th> + <th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Volume</th> + </tr> + </thead> + <tbody> + {stocks.map((stock) => ( + <tr key={stock.symbol} className="border-b border-gray-100 hover:bg-gray-50 transition-colors"> + <td className="px-6 py-4"> + <Link + to={`/analyze/${stock.symbol}`} + className="text-blue-600 font-semibold hover:text-blue-700 hover:underline" + > + {stock.symbol} + </Link> + </td> + <td className="px-6 py-4 text-gray-600">{stock.name}</td> + <td className="px-6 py-4 text-right font-mono text-gray-900">{formatPrice(stock.price)}</td> + <td className={`px-6 py-4 text-right font-mono font-medium ${stock.changePercent >= 0 ? "text-green-600" : "text-red-600"}`}> + {formatChangePercent(stock.changePercent)} + </td> + <td className="px-6 py-4 text-right text-gray-600">{formatVolume(stock.volume)}</td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/components/MostActiveStocks.tsx +git commit -m "feat: add MostActiveStocks table component with auto-refresh" +``` + +--- + +### Task 5: Update stocks.tsx page + +**Files:** +- Modify: `app/routes/stocks.tsx` + +- [ ] **Step 1: Replace StockViewer with MostActiveStocks** + +Replace the entire contents of `app/routes/stocks.tsx`: + +```typescript +import MostActiveStocks from "../components/MostActiveStocks"; +import Navbar from "../components/Navbar"; + +export default function Stocks() { + return ( + <div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50"> + <Navbar /> + <div className="py-20"> + <div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8"> + <div className="text-center mb-12"> + <h1 className="text-4xl font-bold text-gray-900 mb-4"> + Most Active Stocks + </h1> + <p className="text-xl text-gray-600 max-w-2xl mx-auto"> + Real-time view of the most actively traded stocks, auto-refreshing every 30 seconds. + </p> + </div> + <MostActiveStocks /> + </div> + </div> + </div> + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/routes/stocks.tsx +git commit -m "feat: replace StockViewer with MostActiveStocks on stocks page" +``` + +--- + +### Task 6: Verify and test + +**Files:** +- All modified files + +- [ ] **Step 1: Run typecheck** + +```bash +npm run typecheck +``` + +Expected: No type errors. If there are errors, fix them before proceeding. + +- [ ] **Step 2: Run dev server and verify** + +```bash +npm run dev +``` + +Navigate to `http://localhost:5173/stocks` and verify: +- Table loads with most active stocks +- Symbol links navigate to `/analyze/TICKER` +- Change % is color-coded (green for positive, red for negative) +- Data auto-refreshes every 30 seconds +- Loading skeleton shows on initial load +- Error state shows with retry button if API fails + +- [ ] **Step 3: Final commit (if any fixes needed)** + +```bash +git add -A +git commit -m "fix: address typecheck/dev issues" +``` diff --git a/docs/superpowers/specs/2026-05-16-most-active-stocks-table-design.md b/docs/superpowers/specs/2026-05-16-most-active-stocks-table-design.md new file mode 100644 index 0000000..c5c8a0d --- /dev/null +++ b/docs/superpowers/specs/2026-05-16-most-active-stocks-table-design.md @@ -0,0 +1,72 @@ +y# Design: Most Active Stocks Table + +**Date:** 2026-05-16 +**Status:** Approved + +## Overview + +Replace the current StockViewer on the `/stocks` page with a table displaying the most active stocks from Alpaca's screener API. + +## Architecture + +### Server Route: `api/stocks/most-actives` +- **File:** `app/routes/api/stocks/most-actives.ts` +- **Method:** GET (loader) +- **Function:** Proxies request to `https://data.alpaca.markets/v1beta1/screener/stocks/most-actives` +- **Response:** Normalized JSON array with fields: `symbol`, `name`, `price`, `changePercent`, `volume` +- **Auth:** Uses server-side `ALPACA_API_KEY` and `ALPACA_SECRET_KEY` + +### Component: `MostActiveStocks` +- **File:** `app/components/MostActiveStocks.tsx` +- **Behavior:** + - Fetches from `/api/stocks/most-actives` on mount + - Auto-refreshes every 30 seconds via `setInterval` + - Cleans up interval on unmount +- **States:** loading (skeleton rows), success (table), error (red banner with retry button) + +### Page: `stocks.tsx` +- Replaces `<StockViewer />` with `<MostActiveStocks />` +- Updates heading and description text to match new content + +## Data Flow + +``` +MostActiveStocks component + → fetch /api/stocks/most-actives + → server calls Alpaca screener API + → returns normalized data + → renders table + → setInterval re-fetches every 30s +``` + +## Table Columns + +| Column | Source | Format | +|--------|--------|--------| +| Symbol | `symbol` | Clickable link → `/analyze/TICKER` | +| Name | `name` | Plain text | +| Price | `price` | `$X.XX` | +| Change % | `changePercent` | `+X.XX%` / `-X.XX%` (color-coded green/red) | +| Volume | `volume` | Formatted number (e.g., `12.3M`) | + +## Error Handling + +- API errors: display red banner with error message and retry button +- Network failures: show last data if available, otherwise error state +- Empty response: show "No data available" message + +## Route Changes + +Add to `routes.ts`: +``` +route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"), +``` + +## Files Created/Modified + +| File | Action | Purpose | +|------|--------|---------| +| `app/routes/api/stocks/most-actives.ts` | Create | Server proxy to Alpaca | +| `app/components/MostActiveStocks.tsx` | Create | Table component with auto-refresh | +| `app/routes/stocks.tsx` | Modify | Replace StockViewer with MostActiveStocks | +| `app/routes.ts` | Modify | Add new API route | diff --git a/package-lock.json b/package-lock.json index 1c0151c..21d517c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "@modelcontextprotocol/sdk": "^1.29.0", "@react-router/node": "7.15.0", "@react-router/serve": "7.15.0", + "bullmq": "^5.76.8", + "ioredis": "^5.10.1", "isbot": "^5.1.36", "lightweight-charts": "^5.2.0", "react": "^19.2.6", @@ -1367,6 +1369,12 @@ "deprecated": "Use @eslint/object-schema instead", "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1731,6 +1739,84 @@ "node": ">= 0.6" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -3648,6 +3734,23 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "5.76.8", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.8.tgz", + "integrity": "sha512-v3WTwA8diFtsADaJ8eK2ozyi2CYK9rDZCeoKF+dIPF/MUL8HxAOa3H72Gmu1lC4yKlho6t1PwNr/QpDVqaNEZQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.1", + "node-abort-controller": "3.1.1", + "semver": "7.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3783,6 +3886,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3919,6 +4031,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4020,6 +4144,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4053,7 +4186,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5115,6 +5248,30 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -5661,6 +5818,18 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5677,6 +5846,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -5874,6 +6052,37 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/msgpackr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", + "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -5928,6 +6137,27 @@ "node": ">= 0.6" } }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.44", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", @@ -6521,6 +6751,27 @@ "node": ">=8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6752,7 +7003,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6963,6 +7213,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -7183,9 +7439,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", diff --git a/package.json b/package.json index 4aa6b92..e68f3a1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@modelcontextprotocol/sdk": "^1.29.0", "@react-router/node": "7.15.0", "@react-router/serve": "7.15.0", + "bullmq": "^5.76.8", + "ioredis": "^5.10.1", "isbot": "^5.1.36", "lightweight-charts": "^5.2.0", "react": "^19.2.6", diff --git a/playwright-report/index.html b/playwright-report/index.html index 6d03d6d..0d901bd 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -87,4 +87,4 @@ Error generating stack: `+l.message+` <div id='root'></div> </body> </html> -<template id="playwrightReportBase64">data:application/zip;base64,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</template> \ No newline at end of file +<template id="playwrightReportBase64">data:application/zip;base64,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</template> \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index 4876c869d0bf1389afbeb7ea99dd70f8be2239d9..b52d764757f5629ebde06ef688c9e185583a6c0e 100644 GIT binary patch delta 1464 zcma)6O>Epm6t>q1A)D-)9JWEC2&)(V;xJ=-#`Xb1AgKUV&?*V4pduR2jCUt>>}BoU zq)4z$ODZl%pq#iMfw*x%G@?iq61_y?&<h|{MZHlW#3komka*r?L7*bVlE>rs^S#gS zedCKa^ouw2j~<!2qiNa$a1XDsk7wNJJKAjTgWBW|+USI>)oz))6L#&(N%i&Cx7F&U z&={MZp4Q(#v-8vB?Kftu6A8<U1<6&evh;+tym)r`(73oMdA9)T>4g2)(*_eOegDdM zC5v7b2bL#y)a2@jc~85$dvh`{szyD~%y#|9dayh7&{OsP)-j{HvQ-;h)U?rY<WcV9 zz_nxIBHN3EutS8gO*jriKV%ML+>0^s1fhtCzzJx~N;`27C7w$IK^PK2ECcC=97!64 zk-$iJKBXweh`CJv6n+22hYoT80n<KqAQ`VRAy&%<tYwiBgOK;Oj#mcE&XmkTk1$WT zjvX=T*j~Vd%|c)nx!fmQ;uyOQb|Mk`4&lBled+n3=SzerkAx#Jb)_p}8hV(c2y+^F zToOsoV;)B?6%vI4L)~i+$3tq^!33cJGdY|<tYjkcLEO2mnm*v?mi$~o1tVc(J0cEj zj|yxvXpJpB97zG>smpdgd+dd$p~A_-3Lf%_N64y5RWX2kt_t!o%qyCC#k_5PwL4{P zn*Aq?75yl4G0MQBw5jsAv2rB7HDFU2uW#$-&H8rjPtePLt^fSoNOjKEMn_Af^YwNh z90w_c5T-I?og#0A7gDmYcnbS|5P~9{AaE&s-fute9;}zE#>89$5E|O{gj+&H&9$sm zLipWIw&t{>*z0XBy>@m962ANO^D^z$o2@Jn%d*c~sp2fLRC?*f$`I%By8|5I@r@jC zQ2zFMhBBAsiqKbIUzi7k-#5;|`p42+CC<riD%iSA3jkuNuyWX{^4-c+Gi6C`v5vIz zqLZb~<boA-lSJjM8QAm-tC0V25WuC#ItTGt3bE@d5%6edxhhmo4&Zw%=?)I5#oBGj zARM4&!CO+ySiB>_mo_a1xtmg0S+_{C?EI|t5-=?t>r4StmFAY*>;rHYxNWpl0^l1A zsz3-&09JOac1LkA^YV{iWgV;8Rf45p>b3<dWIlU16&Q?*eLL`-kQk0WKA6YhKqJw7 zCxIq94ldGOCyqRl_2%bKpMw5fKX$cj=Jl)drBo~Co@U-P_sl(z>L+INQDaX3-+3BC z?|r9KId0624|4BU{h-*sZr(HZ&0nC{H|Dbq;~9N<acTK)oxyphBM%x4y;P0?^cq7I Qmc~*BmCBgW7%xBNFAF`a<^TWy literal 24576 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 WcxzZ%usmfZEE!mu9y=}t$bJl;Yq=Bv