From ce233aec4ccecdcd533ad0d5792ff3a04ccf7794 Mon Sep 17 00:00:00 2001 From: Aidan Boop Date: Wed, 25 Feb 2026 11:59:36 -0500 Subject: [PATCH 1/4] "Claude PR Assistant workflow" --- .github/workflows/claude.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..d300267f --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + From 03b86405a3bf62f7394577cc3b530512cd74f65e Mon Sep 17 00:00:00 2001 From: Aidan Boop Date: Wed, 25 Feb 2026 11:59:37 -0500 Subject: [PATCH 2/4] "Claude Code Review workflow" --- .github/workflows/claude-code-review.yml | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..b5e8cfd4 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + From 5c652251221474eb76e8df9a289e20659f6da8cb Mon Sep 17 00:00:00 2001 From: aidanboop Date: Wed, 25 Feb 2026 12:49:10 -0500 Subject: [PATCH 3/4] Add accessibility settings panel and fix focus/semantic blockers - Remove all outline:none violations from index.css; add proper button:focus-visible and textarea:focus-visible rules - Add sr-only, skip-link, high-contrast, large-text, reduce-motion, enhanced-focus, and toggle-switch utility CSS classes - Add AccessibilitySettings to ConfigManager defaultConfig - Apply accessibility body classes on app load in App.jsx - Add /accessibility route and sidebar nav link in Loaded.jsx - Add skip link and id="main-content" to LoadedLayout.jsx - Convert ChatbotToggle div.circle to semantic button with aria attrs - Fix hamburger button with aria-label and aria-expanded - Create AccessibilitySettings.jsx page with toggles for HighContrast, ReduceMotion, LargeText, EnhancedFocus, and DarkMode - Add CLAUDE.md project reference for future sessions Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 308 ++++++++++++++++++ open-fin-al/src/Utility/ConfigManager.ts | 6 + .../src/View/AccessibilitySettings.jsx | 156 +++++++++ open-fin-al/src/View/App.jsx | 10 + open-fin-al/src/View/App/Loaded.jsx | 10 +- open-fin-al/src/View/App/LoadedLayout.jsx | 3 +- .../src/View/Chatbot/ChatbotToggle.jsx | 9 +- open-fin-al/src/index.css | 124 ++++++- 8 files changed, 618 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md create mode 100644 open-fin-al/src/View/AccessibilitySettings.jsx diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..cff75420 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,308 @@ +# OpenFinAL — Claude Session Reference + +Open-source AI-enabled financial analytics and investing education platform built as an Electron desktop app. + +--- + +## Tech Stack + +| Layer | Technology | +|---|---| +| Desktop shell | Electron 35.1.5 | +| UI | React 19.1.0 + React Router DOM 7.5.0 | +| Language | TypeScript 5.8.3 (with JSX for View) | +| Database | SQLite via `better-sqlite3` + `sqlite3` | +| Charts | Recharts 2.15.2 | +| Local ML | @xenova/transformers 2.17.2 | +| Credential storage | Keytar (OS keychain) | +| HTTP | Axios + Express proxy (port 3001) | +| Web scraping | Puppeteer 24.6.1 | + +--- + +## Architecture + +Clean Architecture layered stack: + +``` +View (React JSX) + ↓ calls +Interactor (TypeScript — business logic) + ↓ calls +Gateway (TypeScript — data access) + ↓ uses +Entity (TypeScript — data models) + ↓ reads/writes +SQLite DB / External APIs / OS Keychain +``` + +**Entry points:** +- `src/main.js` — Electron main process; IPC handlers; Express proxy; DB setup +- `src/preload.js` — Context bridge; exposes `window.*` APIs to renderer +- `src/renderer.js` — React DOM entry +- `src/View/App.jsx` — Root React component (auth state, initialization, dark mode) + +--- + +## Directory Map + +``` +open-fin-al/src/ +├── View/ React components (see routes below) +│ ├── App.jsx Root: auth, init, dark mode, accessibility body classes +│ ├── App/ +│ │ ├── Loaded.jsx ALL routes defined here; sidebar nav; hamburger menu +│ │ ├── LoadedLayout.jsx HeaderContext (useHeader hook); skip link;
+│ │ ├── Configuring.jsx Initial setup wizard screen +│ │ ├── Preparing.jsx Loading/preparing screen +│ │ └── UserInfo.jsx Profile chip + logout button in header toolset +│ ├── Auth/ PIN-based auth (AuthContainer, PinLogin, PinRegister, PinReset) +│ ├── Home.jsx Dashboard +│ ├── Portfolio.jsx Portfolio management +│ ├── Stock.jsx Stock price / trade (TimeSeries) +│ ├── Stock/ TimeSeriesChart, TickerSearchBar, TickerSidePanel +│ ├── Analysis.jsx Risk analysis +│ ├── StockAnalysis.jsx Stock comparison +│ ├── News.jsx & News/ News browsing (Browser, Listing, Summary, SearchBar) +│ ├── Learn.jsx Learning modules list +│ ├── LearningModule/ LearningModuleDetails, LearningModulePage, Slideshow/* +│ ├── Forecast.jsx Forecasting +│ ├── ForecastFeature.jsx Forecast features demo +│ ├── ForecastModel.jsx Forecast model selection +│ ├── BuyReport.jsx Investment recommendation report +│ ├── Settings.jsx App configuration +│ ├── Settings/ Row, RowLabel, RowValue sub-components +│ ├── AccessibilitySettings.jsx Accessibility toggles page +│ ├── Chatbot.jsx & Chatbot/ AI chatbot (ChatbotToggle, ChatbotMessage) +│ ├── Dashboard/ EconomicChart +│ ├── Shared/ SymbolSearchBar.js +│ └── [Chart components] MovingAVGChart, RSIChart, ROCChart, InvestmentPool, RiskSurvey +│ +├── Interactor/ Business logic (all implement IInputBoundary: post/get/put/delete) +│ ├── InitializationInteractor.ts App init, config/tables creation, data seeding +│ ├── StockInteractor.ts Stock quotes, history, company lookups +│ ├── PortfolioInteractor.ts Portfolio CRUD +│ ├── PortfolioTransactionInteractor.ts +│ ├── UserInteractor.ts User CRUD +│ ├── UserAuthInteractor.ts PIN register/login/reset +│ ├── OrderInteractor.ts Trading orders +│ ├── NewsInteractor.ts News + summarization +│ ├── LearningModuleInteractor.ts Educational content +│ ├── LanguageModelInteractor.ts LLM calls (OpenAI / HuggingFace) +│ ├── SecInteractor.ts SEC XBRL reports +│ ├── FinancialRatioInteractor.ts Ratio calculations +│ ├── MarketStatusInteractor.ts Market open/closed status +│ ├── EconomicIndicatorInteractor.ts +│ └── SettingsInteractor.ts Settings retrieval/updates +│ +├── Gateway/ +│ ├── Data/ +│ │ ├── StockGateway/ AlphaVantage, YFinance, FMP implementations +│ │ ├── NewsGateway/ AlphaVantageNewsGateway +│ │ ├── ReportGateway/ SecAPIGateway (XBRL) +│ │ ├── EconomicGateway/ AlphaVantageEconomicGateway +│ │ ├── RatioGateway/ AlphaVantageRatioGateway +│ │ ├── MarketGateway/ AlphaVantageMarketGateway +│ │ ├── SQLite/ 8 gateways: Portfolio, Asset, User, Transaction, Order, +│ │ │ TableCreation, CompanyLookup, LearningModule +│ │ └── *GatewayFactory.ts Factory per domain (reads config to pick provider) +│ ├── AI/Model/ OpenAIModelGateway, HuggingFaceModelGateway, factories +│ ├── Request/ JSONRequest (wraps JSON string) +│ └── Response/ JSONResponse, XMLResponse +│ +├── Entity/ Data models (IEntity with fields Map, fillWithRequest, getId) +│ User, Portfolio, Asset, PortfolioTransaction, Order, +│ StockRequest, NewsRequest, SecRequest, LanguageModelRequest, +│ LearningModule, LearningPage, EconomicIndicator, MarketStatus, +│ APIEndpoint, Configuration (+ Section + Option), Field +│ +├── Utility/ +│ ├── ConfigManager.ts load/save config; create default config; manage secrets +│ ├── CacheManager.js Response caching with TTL +│ ├── PinEncryption.ts PIN hashing for auth +│ ├── RatioCalculator.ts Financial ratio formulas +│ ├── RequestSplitter.ts Batch/split large API requests +│ └── XBRL/ XBRL.ts + Parser.ts (SEC report parsing) +│ +├── Database/ +│ ├── MigrationManager.js Runs .sql files in /migrations; tracks executed migrations +│ └── migrations/ 001_add_user_auth_fields.sql, 002_make_pinHash_not_null.sql +│ +└── Asset/ + ├── DB/schema.sql SQLite schema + ├── Image/ Logos (logo.png, logo-dark.png, openfinal_logo_no_text.png, etc.) + ├── Slideshows/ Learning module PPTX files + └── LearningModulesVoiceovers/ Audio files +``` + +--- + +## All Routes (`Loaded.jsx`) + +``` +/ Home (Dashboard) +/portfolio Portfolio management +/price Stock trading / TimeSeries +/analysis Risk Analysis +/buy-report Investment report +/news News browsing +/learn Learning module list +/learningModule Module details +/learningModulePage Module page / slide +/forecast Forecasting +/forecast-features Forecast features +/forecast-models Forecast model selection +/StockAnalysis Stock comparison +/settings Configuration +/accessibility Accessibility settings +``` + +All routes are children of `` which provides the header and `HeaderContext`. + +--- + +## Key Patterns + +### Setting a page header +Every page uses `useHeader()` from `LoadedLayout.jsx`: +```jsx +import { useHeader } from "./App/LoadedLayout"; + +const { setHeader } = useHeader(); +useEffect(() => { + setHeader({ title: "My Page", icon: "material_icon_name" }); +}, [setHeader]); +``` + +### Loading / saving config +```js +const config = await window.config.load(); // IPC → main.js → reads default.config.json +config.DarkMode = true; +await window.config.save(config); // IPC → main.js → writes default.config.json +``` + +Default config shape (defined in `ConfigManager.ts → createConfigIfNotExists()`): +```js +{ + DarkMode: false, + MarketStatusGateway, StockGateway, StockQuoteGateway, + NewsGateway, ReportGateway, RatioGateway, EconomicIndicatorGateway, + ChatbotModel, ChatbotModelSettings: { name, maxTokens, temperature, topP }, + NewsSummaryModel, NewsSummaryModelSettings: { ... }, + UserSettings: { FirstName, LastName }, + AccessibilitySettings: { HighContrast, ReduceMotion, LargeText, EnhancedFocus } +} +``` + +### Interactor → Gateway call pattern +```ts +// 1. Create entity, fill from request +const entity = new StockRequest(); +entity.fillWithRequest(requestModel); + +// 2. Create gateway (often via factory reading config) +const factory = new StockGatewayFactory(); +const gateway = await factory.createGateway(); // Reads config.StockGateway + +// 3. Execute +const results = await gateway.read(entity); + +// 4. Return response +const response = new JSONResponse(); +response.convertFromEntity(results, false); +return response.response; +``` + +### Making an IPC/SQLite call from a gateway +```ts +const data = await window.database.SQLiteQuery({ query: "SELECT...", parameters: [id] }); +const row = await window.database.SQLiteGet({ query: "SELECT...", parameters: [id] }); +await window.database.SQLiteInsert({ query: "INSERT...", parameters: [...] }); +await window.database.SQLiteUpdate({ query: "UPDATE...", parameters: [...] }); +await window.database.SQLiteDelete({ query: "DELETE...", parameters: [...] }); +``` + +--- + +## `window.*` APIs (exposed via preload.js) + +| Object | Purpose | +|---|---| +| `window.config` | `load()`, `save(obj)`, `exists()` — config file | +| `window.vault` | `getSecret(key)`, `setSecret(key, val)` — OS keychain | +| `window.database` | `SQLiteQuery`, `SQLiteGet`, `SQLiteInsert`, `SQLiteUpdate`, `SQLiteDelete` | +| `window.yahooFinance` | `chart()`, `search()`, `historical()` | +| `window.exApi` | `get(url)`, `post(url, body)` — SSL-pinned proxy on port 3001 | +| `window.transformers` | `run(model, text)` — local Transformers.js inference | +| `window.file` | `read(path)`, `readBinary(path)` | +| `window.puppetApi` | `getPageText(url)` — Puppeteer web scraping | +| `window.urlWindow` | `open(url)`, `getBodyTextHidden(url)` | +| `window.electronApp` | Electron utility functions | + +--- + +## Authentication + +- PIN-based (8 digits), hashed and stored in `User.pinHash` in SQLite +- Session persisted in `localStorage` as key `openfinAL_user` +- `AuthContainer.jsx` → `PinLogin` / `PinRegister` / `PinReset` +- `UserAuthInteractor.ts` handles all auth logic + +--- + +## Accessibility Features (implemented) + +Body classes toggled by `App.jsx` on load and by `AccessibilitySettings.jsx` on toggle: + +| Body class | Effect | +|---|---| +| `dark-mode` | Dark color scheme (controlled by `config.DarkMode`) | +| `high-contrast` | High-contrast CSS vars (light) or yellow-on-black (+ `dark-mode`) | +| `large-text` | `font-size: 120%` on body | +| `reduce-motion` | Disables all animations/transitions | +| `enhanced-focus` | Larger `outline` + glow on `:focus-visible` elements | + +Skip link: `Skip to main content
@@ -40,7 +41,7 @@ export function AppLoadedLayout(props) {
-
+
diff --git a/open-fin-al/src/View/Chatbot/ChatbotToggle.jsx b/open-fin-al/src/View/Chatbot/ChatbotToggle.jsx index 63d4d100..059a59e1 100644 --- a/open-fin-al/src/View/Chatbot/ChatbotToggle.jsx +++ b/open-fin-al/src/View/Chatbot/ChatbotToggle.jsx @@ -18,9 +18,14 @@ class ChatbotToggle extends Component { return (
-
+
+
{this.state.toggle && ( diff --git a/open-fin-al/src/index.css b/open-fin-al/src/index.css index 97c22402..1edaa898 100644 --- a/open-fin-al/src/index.css +++ b/open-fin-al/src/index.css @@ -150,12 +150,16 @@ button { font-size: 16px; font-weight: bold; border: none; - outline: none; border-radius: 5px; padding: 15px; margin-top: 10px; } +button:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + button:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } @@ -2408,7 +2412,6 @@ nav ul li a:hover { border: 1px solid var(--background-color-highlight); border-radius: 4px; resize: none; - outline: none; overflow-y: scroll; background-color: var(--background-color-highlight-light); font-size: 14px; @@ -2417,6 +2420,11 @@ nav ul li a:hover { height: 71px; } +.chat-input textarea:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + #cross { float: right; position: relative; @@ -2443,7 +2451,6 @@ nav ul li a:hover { padding: 20px; border: 0; margin: 0; - outline: none; font-size: 20px; font-weight: 600; border-radius: 0px; @@ -2656,7 +2663,6 @@ nav ul li a:hover { } .pin-login-input:focus { - outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(43, 56, 143, 0.1); } @@ -3363,3 +3369,113 @@ body.dark-mode .logout-item:hover { + +/* ============================================================ + Accessibility Utilities + ============================================================ */ + +/* Screen-reader only text */ +.sr-only { + position: absolute; width: 1px; height: 1px; + padding: 0; margin: -1px; overflow: hidden; + clip: rect(0,0,0,0); white-space: nowrap; border: 0; +} + +/* Skip navigation link */ +.skip-link { + position: absolute; top: -100%; left: 0; + background: var(--primary-color); color: var(--text-color-white); + padding: 8px 16px; z-index: 10000; + border-radius: 0 0 4px 0; text-decoration: none; font-weight: bold; +} +.skip-link:focus { top: 0; } + +/* High Contrast Mode */ +body.high-contrast { + --primary-color: #0000CC; + --primary-color-hover: #0000FF; + --background-color: #FFFFFF; + --background-color-highlight-light: #EEEEEE; + --background-color-highlight: #CCCCCC; + --text-color-dark: #000000; + --text-color-medium: #111111; + --text-color-medium-dark: #000000; + --accent-color: #0000CC; + --row-primary-color: #FFFFFF; + --row-alternating-color: #DDDDDD; +} +body.high-contrast.dark-mode { + --primary-color: #FFFF00; + --primary-color-hover: #FFFF66; + --background-color: #000000; + --background-color-highlight-light: #111111; + --background-color-highlight: #222222; + --text-color-dark: #FFFFFF; + --text-color-medium: #EEEEEE; + --text-color-medium-dark: #FFFFFF; + --accent-color: #FFFF00; + --row-primary-color: #000000; + --row-alternating-color: #222222; +} + +/* Large Text Mode */ +body.large-text { font-size: 120%; } + +/* Reduce Motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +body.reduce-motion *, body.reduce-motion *::before, body.reduce-motion *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; +} + +/* Enhanced Focus Indicators */ +body.enhanced-focus *:focus-visible { + outline: 3px solid var(--primary-color) !important; + outline-offset: 3px !important; + box-shadow: 0 0 0 5px rgba(43,56,143,0.25) !important; +} + +/* Toggle switch component (used in AccessibilitySettings) */ +.toggle-row { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 0; border-bottom: 1px solid var(--background-color-highlight-light); +} +.toggle-row:last-child { border-bottom: none; } +.toggle-info { flex: 1; margin-right: 20px; } +.toggle-info h4 { margin: 0 0 4px 0; font-size: 1rem; color: var(--text-color-dark); } +.toggle-info p { margin: 0; font-size: 0.85rem; color: var(--text-color-medium); } +.toggle-switch { position: relative; display: inline-block; width: 52px; height: 28px; flex-shrink: 0; } +.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; } +.toggle-slider { + position: absolute; cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--background-color-highlight); + border-radius: 28px; transition: background-color 0.2s; +} +.toggle-slider::before { + content: ''; position: absolute; + height: 20px; width: 20px; left: 4px; bottom: 4px; + background-color: var(--text-color-white); border-radius: 50%; + transition: transform 0.2s; +} +.toggle-switch input:checked + .toggle-slider { background-color: var(--primary-color); } +.toggle-switch input:checked + .toggle-slider::before { transform: translateX(24px); } +.toggle-switch input:focus-visible + .toggle-slider { + outline: 2px solid var(--primary-color); outline-offset: 2px; +} + +/* Accessibility settings page */ +.accessibility-settings { max-width: 700px; margin: 0 auto; } +.accessibility-settings .settings-card { margin-bottom: 20px; } +.accessibility-status { + padding: 10px 14px; border-radius: 6px; font-size: 0.9rem; + background-color: var(--secondary-color); color: var(--text-color-white); + margin-bottom: 16px; display: inline-block; +} From 09bdf7023144d1e4f220d3f7717389a77ace3cef Mon Sep 17 00:00:00 2001 From: aidanboop Date: Mon, 30 Mar 2026 10:51:52 -0400 Subject: [PATCH 4/4] Update CLAUDE.md with toggle switch UI docs and fix package name Documents .toggle-switch and .toggle-slider CSS classes used by AccessibilitySettings.jsx. Corrects package-lock.json name to match repo name (OpenFinAL-TeamAcc). Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 1 + package-lock.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index cff75420..9ff48164 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -264,6 +264,7 @@ Body classes toggled by `App.jsx` on load and by `AccessibilitySettings.jsx` on Skip link: `