From 2f1fe5b39aa2fcfe263a98f090c5438514fa35a3 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 20:40:04 +0200 Subject: [PATCH] Add settings page redesign implementation plan --- .../2026-05-16-settings-page-redesign.md | 960 ++++++++++++++++++ 1 file changed, 960 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-16-settings-page-redesign.md diff --git a/docs/superpowers/plans/2026-05-16-settings-page-redesign.md b/docs/superpowers/plans/2026-05-16-settings-page-redesign.md new file mode 100644 index 0000000..553956a --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-settings-page-redesign.md @@ -0,0 +1,960 @@ +# 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" : ""} +
+ +
+ + + + Ticker + + + + Created + Updated + + + + {paged.map((stock) => ( + + + + + + + + + ))} + +
NotesLast DecisionLast 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}
+