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>

This commit is contained in:
2026-05-16 15:13:38 +02:00
parent 3ed894015a
commit 329b83a17c
11 changed files with 1910 additions and 106 deletions
@@ -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"
```
@@ -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 |