diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d7a1b9..68315a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,9 @@ jobs: run: npm install - name: Test + run: npm test + + - name: Lint and typecheck env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} USERNAME: ${{ secrets.USERNAME }} diff --git a/docs/BrainWaves Technical Implementation Plan (2026 Modernization).md b/docs/BrainWaves Technical Implementation Plan (2026 Modernization).md new file mode 100644 index 0000000..2237e40 --- /dev/null +++ b/docs/BrainWaves Technical Implementation Plan (2026 Modernization).md @@ -0,0 +1,104 @@ +# BrainWaves: Technical Implementation Plan (2026 Modernization) + +**Author:** Manus AI +**Date:** March 2026 + +This document outlines a step-by-step technical implementation plan for AI agents working on the BrainWaves codebase. The goal is to modernize the stack, resolve deployment blockers, and replace the UI library, while establishing a robust testing harness. + +*Note: The migration to Lab Streaming Layer (LSL) is explicitly excluded from this phase and will be addressed once the application is successfully building and deploying.* + +--- + +## Phase 1: Test Harness & Build Environment Setup + +Before modifying the application logic, we must establish a reliable testing harness using Vitest and ensure the Electron build pipeline is functional. The current codebase uses a legacy Jest configuration (`"test": "cross-env jest --passWithNoTests"`) which must be replaced. + +### Step 1.1: Install and Configure Vitest +* **Action:** Remove Jest dependencies and install Vitest, `@testing-library/react`, `@testing-library/jest-dom`, and `jsdom`. +* **Action:** Create a `vitest.config.ts` file configured for a React/DOM environment. +* **Action:** Update `package.json` scripts to use Vitest (`"test": "vitest run"`, `"test:watch": "vitest"`). +* **Acceptance Criteria:** + * A basic sanity test (`src/renderer/App.test.tsx`) rendering a simple component passes using `npm test`. + * The CI workflow (`.github/workflows/test.yml`) is updated to run `npm test` using Vitest. + +### Step 1.2: Verify Electron Build Pipeline +* **Action:** Run the existing `npm run build` and `npm run package-ci` commands to identify any immediate build failures caused by Node 22 or Vite 7. +* **Action:** Fix any immediate compilation errors in `src/main/index.ts` or `vite.config.ts`. +* **Acceptance Criteria:** + * `npm run build` completes without errors. + * An integration test (`tests/build.test.ts`) is added that programmatically verifies the existence of the `out/main/index.js` and `out/renderer/index.html` files after a build. + +--- + +## Phase 2: Routing and State Synchronization Modernization + +The codebase currently relies on `react-router-dom v5` and the abandoned `connected-react-router` (which syncs router state to Redux). This causes significant issues with modern React 18 and Redux Toolkit. + +### Step 2.1: Remove `connected-react-router` +* **Action:** Uninstall `connected-react-router`. +* **Action:** Remove `routerMiddleware` and `connectRouter` from `src/renderer/store.ts` and `src/renderer/reducers/index.ts`. +* **Action:** Remove `ConnectedRouter` from `src/renderer/containers/Root.tsx` and replace it with a standard `HashRouter` from `react-router-dom`. +* **Acceptance Criteria:** + * The Redux store initializes successfully without the router reducer. + * Unit tests verify that the Redux store can be created with initial state (`tests/store.test.ts`). + +### Step 2.2: Upgrade to React Router v6/v7 +* **Action:** Upgrade `react-router-dom` to the latest stable version. +* **Action:** Refactor `src/renderer/routes.tsx` to use the new `` and `}>` syntax instead of the legacy `` and `component={}` props. +* **Action:** Refactor class components (e.g., `HomeComponent`, `DesignComponent`) that rely on `this.props.history.push`. Convert them to functional components using the `useNavigate` hook, or create a Higher-Order Component (HOC) `withRouter` wrapper if functional conversion is too extensive. +* **Action:** Update `src/renderer/epics/experimentEpics.ts` which currently listens to `@@router/LOCATION_CHANGE`. Refactor the logic to trigger based on specific Redux actions rather than URL changes. +* **Acceptance Criteria:** + * Unit tests (`tests/routing.test.tsx`) verify that navigation between the Home, Design, and Collect screens renders the correct components. + +--- + +## Phase 3: Pyodide and Python Dependency Modernization + +The application uses Pyodide `v0.21.0` (hardcoded in `internals/scripts/InstallPyodide.js`) and relies on deprecated Python libraries for data visualization. + +### Step 3.1: Upgrade Pyodide +* **Action:** Update `internals/scripts/InstallPyodide.js` to download a modern stable release of Pyodide (e.g., `v0.27.0`). +* **Action:** Ensure the `vite.config.ts` `publicDir` setting still correctly serves the updated Pyodide WASM and JS files. +* **Acceptance Criteria:** + * An integration test (`tests/pyodide.test.ts`) successfully instantiates the Pyodide web worker (`src/renderer/utils/pyodide/webworker.js`) and executes a simple `1 + 1` Python command. + +### Step 3.2: Refactor Python Plotting Logic +* **Action:** In `src/renderer/utils/pyodide/utils.py`, locate the usage of `sns.tsplot` (which was removed in Seaborn v0.10.0). +* **Action:** Rewrite the `plot_conditions` function to use `sns.lineplot` or standard `matplotlib.pyplot.plot` with `fill_between` for confidence intervals. +* **Acceptance Criteria:** + * A unit test (`tests/python_utils.test.ts`) loads `utils.py` into the Pyodide worker, passes mock EEG data, and verifies that the plotting function executes without throwing a Python `AttributeError`. + +--- + +## Phase 4: UI Library Replacement (Semantic UI to Shadcn/ui) + +`semantic-ui-react` is abandoned and throws deprecation warnings in React 18. We will replace it with Tailwind CSS and Shadcn/ui components. + +### Step 4.1: Install Tailwind CSS and Shadcn/ui +* **Action:** Install Tailwind CSS, PostCSS, and Autoprefixer. Configure `tailwind.config.js` and `postcss.config.js`. +* **Action:** Initialize Shadcn/ui (`npx shadcn-ui@latest init`) and configure it to output components to `src/renderer/components/ui`. +* **Acceptance Criteria:** + * A unit test verifies that a basic Shadcn/ui `Button` component renders correctly with Tailwind utility classes applied. + +### Step 4.2: Component-by-Component Replacement +* **Action:** Identify the ~26 files importing `semantic-ui-react`. The most heavily used components are `Segment`, `Button`, `Grid`, and `Header`. +* **Action:** Replace `semantic-ui-react` components with their Shadcn/ui equivalents: + * `Button` -> Shadcn `Button` + * `Segment` -> Shadcn `Card` or a simple `div` with Tailwind borders/padding. + * `Grid` -> Tailwind CSS Grid (`grid grid-cols-12 gap-4`). + * `Header` -> Standard HTML `h1`-`h6` tags with Tailwind typography classes. + * `Modal` -> Shadcn `Dialog`. +* **Action:** Remove `semantic-ui-css` from `src/renderer/index.tsx` and uninstall `semantic-ui-react`. +* **Acceptance Criteria:** + * `grep -r "semantic-ui-react" src/` returns zero results. + * Visual regression or DOM snapshot tests (`tests/ui_migration.test.tsx`) for key screens (Home, Design, Collect) pass, ensuring the new components render without crashing. + +--- + +## Phase 5: Final Build and Verification + +### Step 5.1: End-to-End Build Verification +* **Action:** Run the full build pipeline (`npm run package-all`). +* **Acceptance Criteria:** + * The Electron application compiles and packages successfully for the target OS. + * All Vitest suites (Routing, Store, Pyodide, UI) pass with 100% success rate. diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 0000000..46caf52 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,353 @@ +# BrainWaves Modernization — Implementation Progress + +Tracking file for executing the [Technical Implementation Plan](./BrainWaves_%20Technical%20Implementation%20Plan%20(2026%20Modernization).md) across sessions. + +**Last updated:** 2026-03-07 +**Overall status:** PHASES 1–4 COMPLETE (Phase 5 pending: npm install + build verification) + +--- + +## Codebase Reconnaissance (completed) + +Key files read and understood before starting implementation: + +- `package.json` — current deps, jest config, scripts +- `src/renderer/store.ts` — uses `connected-react-router` (`routerMiddleware`, `createHashHistory`) +- `src/renderer/reducers/index.ts` — uses `connectRouter` from `connected-react-router` +- `src/renderer/containers/Root.tsx` — uses `ConnectedRouter`, receives `history` prop +- `src/renderer/index.tsx` — imports and passes `history` from store to Root +- `src/renderer/routes.tsx` — React Router v5 `` / `` / custom `PropsRoute` +- `src/renderer/epics/experimentEpics.ts` — `autoSaveEpic` and `navigationCleanupEpic` listen to `@@router/LOCATION_CHANGE` +- `src/renderer/containers/TopNavBarContainer.ts` — maps `state.router.location` to props +- `src/renderer/components/TopNavComponent/index.tsx` — class component, uses `this.props.location.pathname` +- `src/renderer/components/HomeComponent/index.tsx` — class component, calls `this.props.history.push()` +- `src/renderer/components/CollectComponent/index.tsx` — passes `history` prop to `ConnectModal` (unused there) +- `src/renderer/components/CollectComponent/ConnectModal.tsx` — has `history` in Props but does NOT use it +- `src/renderer/components/EEGExplorationComponent.tsx` — passes `history` to `ConnectModal` (unused) +- `src/renderer/actions/experimentActions.ts` — RTK `createAction`, `typesafe-actions` +- `src/renderer/utils/pyodide/utils.py` — uses `sns.tsplot` (removed in seaborn v0.10), seaborn import commented out +- `src/renderer/utils/pyodide/webworker.js` — `importScripts('/pyodide/pyodide.js')`, loads matplotlib/mne/pandas +- `internals/scripts/InstallPyodide.js` — downloads pyodide v0.21.0 tarball +- `vite.config.ts` — `publicDir` serves `src/renderer/utils/pyodide/src/` as static assets +- `.github/workflows/test.yml` — runs `npm run package-ci`, lint, tsc (no unit tests) +- 26 files import `semantic-ui-react` (see list below) + +--- + +## Phase 1: Test Harness & Vitest + +**Status: COMPLETE** + +### Step 1.1 — Install and configure Vitest + +**What to do:** +1. Edit `package.json`: + - Remove from `devDependencies`: `jest`, `@types/jest`, `identity-obj-proxy`, `react-test-renderer` + - Add to `devDependencies`: `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom` + - Change `"test": "cross-env jest --passWithNoTests"` → `"test": "vitest run"` + - Add `"test:watch": "vitest"` + - Remove the `"jest": { ... }` config block from package.json +2. Create `vitest.config.ts` (project root) +3. Create `src/test-setup.ts` (imports `@testing-library/jest-dom`) +4. Create `src/renderer/App.test.tsx` (basic sanity render test) +5. Run `npm install` to update node_modules + +**Notes:** +- Vitest needs `jsdom` environment for React component tests +- The vite.config.ts babel plugins (decorators, class-properties) must also be present in vitest.config.ts +- CSS modules: vitest handles them natively with `css.modules` config; no `identity-obj-proxy` needed + +### Step 1.2 — Update CI workflow + +**What to do:** +1. Edit `.github/workflows/test.yml`: add `npm test` step before or after existing steps +2. Create `tests/build.test.ts`: verifies `out/main/index.js` and `out/renderer/index.html` exist after build + +--- + +## Phase 2: Routing Modernization + +**Status: COMPLETE** + +### Step 2.1 — Remove `connected-react-router` + +**Package changes:** +- Remove from `dependencies`: `connected-react-router`, `history` +- Remove from `devDependencies`: `@types/history`, `@types/react-router`, `@types/react-router-dom` +- Remove `overrides["connected-react-router"]` block + +**File changes:** + +| File | Change | +|------|--------| +| `src/renderer/store.ts` | Remove `createHashHistory`, `history` export, `routerMiddleware`, remove `router` from middleware | +| `src/renderer/reducers/index.ts` | Remove `connectRouter`, `History` import, remove `router` from combineReducers, remove `router: any` from RootState | +| `src/renderer/containers/Root.tsx` | Remove `ConnectedRouter`; use `HashRouter` from `react-router-dom`; remove `history` prop | +| `src/renderer/index.tsx` | Remove `history` import; pass only `store` to `` | + +### Step 2.2 — Upgrade to React Router v6 + +**Package changes:** +- Change `react-router` and `react-router-dom`: `"^5.2.0"` → `"^6.x"` (v6 merges the two packages; keep both entries or consolidate) + +**New file:** +- `src/renderer/actions/routerActions.ts` — defines `RouterActions.RouteChanged(pathname: string)` action + +**File changes:** + +| File | Change | +|------|--------| +| `src/renderer/routes.tsx` | Replace `/` with `/}>`. Remove `PropsRoute`. Pass `activeStep` directly as JSX prop on ``. | +| `src/renderer/containers/App.tsx` | Add `NavigationTracker` functional component (uses `useLocation` + `useDispatch` to dispatch `RouterActions.RouteChanged` on location change) | +| `src/renderer/epics/experimentEpics.ts` | Replace both `ofType('@@router/LOCATION_CHANGE')` epics to use `filter(isActionOf(RouterActions.RouteChanged))`. Access `action.payload` as pathname string directly. Remove `pluck('payload', 'pathname')` etc. | +| `src/renderer/components/TopNavComponent/index.tsx` | Convert class → functional component. Replace `this.props.location.pathname` with `useLocation().pathname`. State becomes `useState`. Methods become callbacks. | +| `src/renderer/containers/TopNavBarContainer.ts` | Remove `location: state.router.location` from mapStateToProps | +| `src/renderer/components/HomeComponent/index.tsx` | Change `history: History` prop to `navigate: (path: string) => void`. Replace all `this.props.history.push(X)` with `this.props.navigate(X)`. Remove `import { History } from 'history'`. | +| `src/renderer/containers/HomeContainer.ts` | Wrap exported component with `withNavigate` HOC that injects `useNavigate()` as `navigate` prop | +| `src/renderer/components/CollectComponent/index.tsx` | Remove `history: History` from Props; remove passing `history` to `ConnectModal` | +| `src/renderer/components/EEGExplorationComponent.tsx` | Remove `history: History` from Props; remove passing `history` to `ConnectModal` | +| `src/renderer/components/CollectComponent/ConnectModal.tsx` | Remove `history: History` from Props interface | + +**withNavigate HOC pattern (for HomeContainer.ts):** +```tsx +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +function withNavigate

void }>( + Component: React.ComponentType

+) { + return function WithNavigate(props: Omit) { + const navigate = useNavigate(); + return ; + }; +} +``` + +**NavigationTracker pattern (for App.tsx):** +```tsx +function NavigationTracker() { + const location = useLocation(); + const dispatch = useDispatch(); + useEffect(() => { + dispatch(RouterActions.RouteChanged(location.pathname)); + }, [location.pathname, dispatch]); + return null; +} +``` + +**Tests to add:** +- `tests/store.test.ts` — verifies store initializes without router reducer +- `tests/routing.test.tsx` — verifies navigation between Home/Design/Collect renders correct components + +--- + +## Phase 3: Pyodide Modernization + +**Status: COMPLETE** + +### Step 3.1 — Upgrade Pyodide version + +**File:** `internals/scripts/InstallPyodide.js` + +Changes: +- `PYODIDE_VERSION`: `'0.21.0'` → `'0.27.0'` +- `TAR_NAME`: `pyodide-build-${PYODIDE_VERSION}.tar.bz2` → `pyodide-${PYODIDE_VERSION}.tar.bz2` +- `TAR_URL`: update to match new naming + +**Note:** The tarball for 0.27.0 extracts to a `pyodide/` subdirectory, which is correct for the webworker's `importScripts('/pyodide/pyodide.js')` call. The old 0.21.0 tar was also `pyodide-build-*` but extracted to a `pyodide/` dir. + +**Caution:** `mne` package availability in pyodide 0.27.0 needs verification. If not in the default package list, `webworker.js` `loadPackage(['matplotlib', 'mne', 'pandas'])` will fail. May need to load `mne` via `micropip.install('mne')` instead. + +**Test to add:** `tests/pyodide.test.ts` + +### Step 3.2 — Fix Python plotting (`utils.py`) + +**File:** `src/renderer/utils/pyodide/utils.py` + +The `plot_conditions` function currently: +- Calls `sns.color_palette(...)` — but `import seaborn as sns` is commented out +- Calls `sns.tsplot(...)` — removed from seaborn in v0.10.0 +- Calls `sns.despine()` — still exists but seaborn not imported + +**Fix — replace `plot_conditions` body:** +1. Replace `sns.color_palette(...)` with a hardcoded palette (consistent with `plot_topo`) +2. Replace `sns.tsplot(...)` with manual bootstrap CI using numpy + `ax.plot()` + `ax.fill_between()` +3. Replace `sns.despine()` with `ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)` + +**Bootstrap CI replacement for `sns.tsplot(X[...], time=times, color=color, n_boot=n_boot, ci=ci)`:** +```python +cond_data = X[y.isin(cond), ch_ind] +mean = np.nanmean(cond_data, axis=0) +n_samples = cond_data.shape[0] +boot_means = np.array([ + np.nanmean(cond_data[np.random.randint(0, n_samples, n_samples)], axis=0) + for _ in range(n_boot) +]) +alpha = (100 - ci) / 2 +low = np.percentile(boot_means, alpha, axis=0) +high = np.percentile(boot_means, 100 - alpha, axis=0) +ax.plot(times, mean, color=color) +ax.fill_between(times, low, high, color=color, alpha=0.3) +``` + +**Test to add:** `tests/python_utils.test.ts` + +--- + +## Phase 4: UI Library Replacement (Semantic UI → Tailwind + Shadcn/ui) + +**Status: COMPLETE** + +### Step 4.1 — Install Tailwind CSS and Shadcn/ui + +**Package additions (devDependencies):** +- `tailwindcss`, `postcss`, `autoprefixer` + +**Package additions (dependencies):** +- `@radix-ui/react-dialog` +- `@radix-ui/react-dropdown-menu` +- `@radix-ui/react-slot` +- `class-variance-authority` +- `clsx` +- `tailwind-merge` + +**Package removals (dependencies):** +- `semantic-ui-react` +- `semantic-ui-css` + +**New config files:** +- `tailwind.config.js` +- `postcss.config.js` + +**New UI component files** (in `src/renderer/components/ui/`): +- `utils.ts` — `cn()` helper (`clsx` + `tailwind-merge`) +- `button.tsx` — Shadcn Button (CVA variants) +- `card.tsx` — Shadcn Card (replaces `Segment`) +- `dialog.tsx` — Shadcn Dialog (replaces `Modal`) +- `dropdown-menu.tsx` — Shadcn DropdownMenu (replaces `Dropdown`) +- `table.tsx` — Shadcn Table (replaces `Table`) + +**Update `src/renderer/app.global.css`:** add Tailwind directives (`@tailwind base/components/utilities`) + +**Remove from `src/renderer/index.tsx`:** `import 'semantic-ui-css/semantic.min.css'` + +### Step 4.2 — Component-by-component replacement + +**26 files to update** (grep confirmed): + +| File | Semantic UI components used | Status | +|------|-----------------------------|--------| +| `components/AnalyzeComponent.tsx` | Grid, Icon, Segment, Header, Dropdown, Divider, Button, Checkbox, Sidebar, DropdownProps | NOT STARTED | +| `components/CleanComponent/CleanSidebar.tsx` | (need to read) | NOT STARTED | +| `components/CleanComponent/index.tsx` | Grid, Button, Icon, Segment, Header, Dropdown, Sidebar, SidebarPusher, Divider, DropdownProps | NOT STARTED | +| `components/CollectComponent/ConnectModal.tsx` | Modal, Button, Segment, List, Grid, Divider | NOT STARTED | +| `components/CollectComponent/HelpSidebar.tsx` | (need to read) | NOT STARTED | +| `components/CollectComponent/PreTestComponent.tsx` | (need to read) | NOT STARTED | +| `components/CollectComponent/RunComponent.tsx` | (need to read) | NOT STARTED | +| `components/CollectComponent/index.tsx` | (passes through, may be minimal) | NOT STARTED | +| `components/DesignComponent/CustomDesignComponent.tsx` | (need to read) | NOT STARTED | +| `components/DesignComponent/ParamSlider.tsx` | (need to read) | NOT STARTED | +| `components/DesignComponent/StimuliDesignColumn.tsx` | (need to read) | NOT STARTED | +| `components/DesignComponent/StimuliRow.tsx` | (need to read) | NOT STARTED | +| `components/DesignComponent/index.tsx` | (need to read) | NOT STARTED | +| `components/EEGExplorationComponent.tsx` | Grid, Button, Header, Segment, Image, Divider | NOT STARTED | +| `components/HomeComponent/ExperimentCard.tsx` | (need to read) | NOT STARTED | +| `components/HomeComponent/OverviewComponent.tsx` | (need to read) | NOT STARTED | +| `components/HomeComponent/index.tsx` | Grid, Button, Header, Image, Table | NOT STARTED | +| `components/InputCollect.tsx` | (need to read) | NOT STARTED | +| `components/InputModal.tsx` | (need to read) | NOT STARTED | +| `components/PreviewButtonComponent.tsx` | (need to read) | NOT STARTED | +| `components/PreviewExperimentComponent.tsx` | (need to read) | NOT STARTED | +| `components/PyodidePlotWidget.tsx` | (need to read) | NOT STARTED | +| `components/SecondaryNavComponent/SecondaryNavSegment.tsx` | (need to read) | NOT STARTED | +| `components/SecondaryNavComponent/index.tsx` | (need to read) | NOT STARTED | +| `components/SignalQualityIndicatorComponent.tsx` | (need to read) | NOT STARTED | +| `components/TopNavComponent/PrimaryNavSegment.tsx` | (need to read) | NOT STARTED | +| `components/TopNavComponent/index.tsx` | Grid, Segment, Image, Dropdown | NOT STARTED (also being changed in Phase 2) | + +**Replacement mapping:** +| Semantic UI | Replacement | +|-------------|-------------| +| `` | `

` (Tailwind) | +| `` | `
` or grid row | +| `` | Tailwind `col-span-N` | +| `` | `
` or `` | +| `