# Settings Page Redesign 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 bare-bones settings page with a structured dashboard featuring sidebar navigation, typed LLM/trading settings, and an editable stock database table. **Architecture:** Client-side SPA using `useEffect` to fetch settings and stocks on mount. Settings stored as structured keys in existing `AppSetting` table. Saves via existing `PUT /api/admin/settings/:key` endpoint with optimistic UI updates. Stock notes saved via `POST /api/stocks`. **Tech Stack:** React Router 7, TypeScript, TailwindCSS, Prisma (SQLite) --- ### Task 1: Extend stocks API to support notes field **Files:** - Modify: `app/routes/api/stocks/index.ts` - Test: `tests/stock-db.spec.ts` - [ ] **Step 1: Add notes to the stocks API action** The stocks API currently does not save or return the `notes` field. Add it to the upsert: ```typescript // In app/routes/api/stocks/index.ts, add notes extraction after lastJobId: const notes = formData.get("notes")?.toString(); // Update the upsert to include notes in both update and create: const stock = await db.stock.upsert({ where: { ticker }, update: { lastDecision: lastDecision ?? undefined, lastExplanation: lastExplanation ?? undefined, lastExecutionPlan: lastExecutionPlan ?? undefined, lastJobId: lastJobId ?? undefined, notes: notes ?? undefined, }, create: { ticker, lastDecision: lastDecision ?? undefined, lastExplanation: lastExplanation ?? undefined, lastExecutionPlan: lastExecutionPlan ?? undefined, lastJobId: lastJobId ?? undefined, notes: notes ?? undefined, }, }); ``` - [ ] **Step 2: Run Playwright test to verify stock DB still works** Run: `npx playwright test tests/stock-db.spec.ts` Expected: PASS (existing tests should still pass since we're only adding a field) - [ ] **Step 3: Commit** ```bash git add app/routes/api/stocks/index.ts git commit -m "feat: add notes field to stocks API upsert" ``` --- ### Task 2: Create SettingsSidebar component **Files:** - Create: `app/components/SettingsSidebar.tsx` - [ ] **Step 1: Create the SettingsSidebar component** ```typescript import React from "react"; export type SettingsSection = "llm" | "trading" | "stocks" | "system"; interface SettingsSidebarProps { activeSection: SettingsSection; onSectionChange: (section: SettingsSection) => void; } const sections: { id: SettingsSection; label: string; icon: string }[] = [ { id: "llm", label: "LLM & Agents", icon: "🧠" }, { id: "trading", label: "Trading Defaults", icon: "📊" }, { id: "stocks", label: "Stock Database", icon: "📋" }, { id: "system", label: "System", icon: "⚙️" }, ]; export default function SettingsSidebar({ activeSection, onSectionChange }: SettingsSidebarProps) { return ( ); } ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit app/components/SettingsSidebar.tsx` Expected: No errors - [ ] **Step 3: Commit** ```bash git add app/components/SettingsSidebar.tsx git commit -m "feat: add SettingsSidebar component with section navigation" ``` --- ### Task 3: Create LlmSettings component **Files:** - Create: `app/components/LlmSettings.tsx` - [ ] **Step 1: Create the LlmSettings component** ```typescript import React, { useState, useEffect } from "react"; interface LlmSettingsProps { settings: Record; onSave: (key: string, value: any) => Promise; saveError: string | null; } const AVAILABLE_MODELS = [ "openai/gpt-oss-120b:free", "openrouter/free", "deepseek/deepseek-chat:free", "meta/llama-3.3-70b-instruct:free", ]; export default function LlmSettings({ settings, onSave, saveError }: LlmSettingsProps) { const [model, setModel] = useState(settings["llm.model"] ?? "openai/gpt-oss-120b:free"); const [temperature, setTemperature] = useState(settings["llm.temperature"] ?? 0.7); const [maxDebateRounds, setMaxDebateRounds] = useState(settings["llm.maxDebateRounds"] ?? 3); useEffect(() => { setModel(settings["llm.model"] ?? "openai/gpt-oss-120b:free"); setTemperature(settings["llm.temperature"] ?? 0.7); setMaxDebateRounds(settings["llm.maxDebateRounds"] ?? 3); }, [settings]); const saveModel = async (value: string) => { setModel(value); await onSave("llm.model", value); }; const saveTemperature = async (value: number) => { setTemperature(value); await onSave("llm.temperature", value); }; const saveMaxDebateRounds = async (value: number) => { const clamped = Math.min(10, Math.max(1, value)); setMaxDebateRounds(clamped); await onSave("llm.maxDebateRounds", clamped); }; return (

LLM & Agents

Configure the language model and agent behavior for trading analysis.

{saveError && (
{saveError}
)}
saveTemperature(parseFloat(e.target.value))} className="w-full" />
0.0 (deterministic) 2.0 (creative)
saveMaxDebateRounds(parseInt(e.target.value) || 1)} className="w-32 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500" />
); } ``` - [ ] **Step 2: Commit** ```bash git add app/components/LlmSettings.tsx git commit -m "feat: add LlmSettings component with model, temperature, debate rounds" ``` --- ### Task 4: Create TradingSettings component **Files:** - Create: `app/components/TradingSettings.tsx` - [ ] **Step 1: Create the TradingSettings component** ```typescript import React, { useState, useEffect } from "react"; interface TradingSettingsProps { settings: Record; onSave: (key: string, value: any) => Promise; saveError: string | null; } export default function TradingSettings({ settings, onSave, saveError }: TradingSettingsProps) { const [maxLossPercent, setMaxLossPercent] = useState(settings["trading.maxLossPercent"] ?? 2); const [positionSizePercent, setPositionSizePercent] = useState(settings["trading.positionSizePercent"] ?? 10); const [takeProfitPercent, setTakeProfitPercent] = useState(settings["trading.takeProfitPercent"] ?? 5); const [stopLossPercent, setStopLossPercent] = useState(settings["trading.stopLossPercent"] ?? 3); const [riskMethod, setRiskMethod] = useState(settings["trading.riskMethod"] ?? "percentage"); useEffect(() => { setMaxLossPercent(settings["trading.maxLossPercent"] ?? 2); setPositionSizePercent(settings["trading.positionSizePercent"] ?? 10); setTakeProfitPercent(settings["trading.takeProfitPercent"] ?? 5); setStopLossPercent(settings["trading.stopLossPercent"] ?? 3); setRiskMethod(settings["trading.riskMethod"] ?? "percentage"); }, [settings]); const save = async (key: string, value: any) => { await onSave(key, value); }; return (

Trading Defaults

Default risk management and position sizing parameters.

{saveError && (
{saveError}
)}
{ const v = parseFloat(e.target.value) || 0; setMaxLossPercent(v); save("trading.maxLossPercent", v); }} className="w-32 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500" />

Maximum portfolio percentage to risk on a single trade.

{ const v = parseInt(e.target.value) || 1; setPositionSizePercent(v); save("trading.positionSizePercent", v); }} className="w-32 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500" />

Default position size as percentage of available cash.

{ const v = parseFloat(e.target.value) || 0; setTakeProfitPercent(v); save("trading.takeProfitPercent", v); }} className="w-32 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500" />

Target profit percentage for auto take-profit orders.

{ const v = parseFloat(e.target.value) || 0; setStopLossPercent(v); save("trading.stopLossPercent", v); }} className="w-32 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500" />

Stop loss percentage below entry price.

); } ``` - [ ] **Step 2: Commit** ```bash git add app/components/TradingSettings.tsx git commit -m "feat: add TradingSettings component with risk management defaults" ``` --- ### Task 5: Create StockTable component **Files:** - Create: `app/components/StockTable.tsx` - [ ] **Step 1: Create the StockTable component** ```typescript import React, { useState, useMemo } from "react"; interface Stock { id: string; ticker: string; notes: string | null; lastDecision: string | null; lastJobId: string | null; createdAt: string; updatedAt: string; } interface StockTableProps { stocks: Stock[]; onNotesSave: (ticker: string, notes: string) => Promise; saveError: string | null; } type SortField = "ticker" | "createdAt" | "updatedAt"; type SortDirection = "asc" | "desc"; export default function StockTable({ stocks, onNotesSave, saveError }: StockTableProps) { const [search, setSearch] = useState(""); const [sortField, setSortField] = useState("ticker"); const [sortDirection, setSortDirection] = useState("asc"); const [editingTicker, setEditingTicker] = useState(null); const [editingNotes, setEditingNotes] = useState(""); const [savingNotes, setSavingNotes] = useState(null); const [page, setPage] = useState(0); const pageSize = 20; const filtered = useMemo(() => { const q = search.toLowerCase(); const result = stocks.filter((s) => s.ticker.toLowerCase().includes(q)); result.sort((a, b) => { const aVal = a[sortField] ?? ""; const bVal = b[sortField] ?? ""; const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return sortDirection === "asc" ? cmp : -cmp; }); return result; }, [stocks, search, sortField, sortDirection]); const totalPages = Math.ceil(filtered.length / pageSize); const paged = filtered.slice(page * pageSize, (page + 1) * pageSize); const handleSort = (field: SortField) => { if (sortField === field) { setSortDirection((d) => (d === "asc" ? "desc" : "asc")); } else { setSortField(field); setSortDirection("asc"); } }; const startEditing = (stock: Stock) => { setEditingTicker(stock.ticker); setEditingNotes(stock.notes ?? ""); }; const saveNotes = async () => { if (!editingTicker) return; setSavingNotes(editingTicker); try { await onNotesSave(editingTicker, editingNotes); } catch (e) { console.error("Failed to save notes:", e); } finally { setSavingNotes(null); setEditingTicker(null); } }; const SortHeader = ({ field, children }: { field: SortField; children: React.ReactNode }) => ( handleSort(field)} > {children} {sortField === field && {sortDirection === "asc" ? "↑" : "↓"}} ); if (stocks.length === 0) { return (

Stock Database

Manage tracked stocks and their analysis notes.

No stocks tracked yet. Visit the stocks page to add some.

); } return (

Stock Database

Manage tracked stocks and their analysis notes.

{saveError && (
{saveError}
)}
{ setSearch(e.target.value); setPage(0); }} className="border border-gray-300 rounded-lg px-4 py-2.5 w-64 focus:ring-2 focus:ring-blue-500" /> {filtered.length} stock{filtered.length !== 1 ? "s" : ""}
TickerCreatedUpdated {paged.map((stock) => ( ))}
Notes Last Decision Last Job
{stock.ticker} {editingTicker === stock.ticker ? ( setEditingNotes(e.target.value)} onBlur={saveNotes} onKeyDown={(e) => { if (e.key === "Enter") saveNotes(); if (e.key === "Escape") setEditingTicker(null); }} className="w-full border border-blue-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500" autoFocus /> ) : ( startEditing(stock)} > {stock.notes || Click to add notes...} )} {stock.lastDecision ? ( {stock.lastDecision.toUpperCase()} ) : "-"} {stock.lastJobId ? ( {stock.lastJobId.slice(0, 12)}... ) : "-"} {new Date(stock.createdAt).toLocaleDateString()} {new Date(stock.updatedAt).toLocaleDateString()}
{filtered.length === 0 && (

No stocks found matching "{search}"

)} {totalPages > 1 && (
Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, filtered.length)} of {filtered.length}
)}
); } ``` - [ ] **Step 2: Commit** ```bash git add app/components/StockTable.tsx git commit -m "feat: add StockTable component with search, sort, pagination, inline editing" ``` --- ### Task 6: Create SystemSettings component **Files:** - Create: `app/components/SystemSettings.tsx` - [ ] **Step 1: Create the SystemSettings component** ```typescript import React, { useState, useEffect } from "react"; interface SystemSettingsProps { settings: Record; alpacaMode: string | null; onSave: (key: string, value: any) => Promise; saveError: string | null; } const KNOWN_KEYS = new Set([ "llm.model", "llm.temperature", "llm.maxDebateRounds", "trading.maxLossPercent", "trading.positionSizePercent", "trading.takeProfitPercent", "trading.stopLossPercent", "trading.riskMethod", ]); export default function SystemSettings({ settings, alpacaMode, onSave, saveError }: SystemSettingsProps) { const [rawSettings, setRawSettings] = useState>([]); useEffect(() => { const others = Object.entries(settings) .filter(([key]) => !KNOWN_KEYS.has(key)) .map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value, null, 2), })); setRawSettings(others); }, [settings]); const handleRawSave = async (key: string, newValue: string) => { try { const parsed = JSON.parse(newValue); await onSave(key, parsed); } catch { await onSave(key, newValue); } }; return (

System

System configuration and environment info.

{saveError && (
{saveError}
)}

Alpaca Trading API

Mode: {alpacaMode === "live" ? "Live Trading" : "Paper Trading"}
{rawSettings.length > 0 && (

Additional Settings

{rawSettings.map((setting) => (
{setting.key}