remove superseded design notes

CLAUDE.md already told readers these were "older design notes" and to treat the code as
the source of truth. Keeping them is worse than that warning: they are detailed enough to
be believed, and every one of them describes an app that has since changed.

- OFFICERDEV_BACKEND.md, OFFICERDEV_FRONTEND.md — architecture snapshots from February;
  CLAUDE.md and the code cover this now.
- SETUP_ANALYSIS.md — an analysis of problems in setup.sh, superseded by `bun setup`.
- event-handler-instances.md — the result of a one-off scan ("found 167 instances"),
  regenerable with a grep in less time than reading it.
- HOOKS.md — a bare list of hook file paths, likewise.
- docs/per-user-api-keys.md — an architecture analysis for per-user API keys, which
  contradicts the single-user hard invariant in CLAUDE.md.

Kept deliberately, though all are old: SECURITY_AUDIT.md and SECURITY_FIXES.md (a
findings record and its remediation log — not the kind of thing to bin on a hunch),
PHONE_APP.md (a native app is being built), MARKETING_WEBSITE.md and
docs/DOCKERIZATION_PLAN.md (plans that may not have been carried out yet), and
docs/jobs-unification.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 08:31:14 +00:00
co-authored by Claude Opus 5
parent 71c32b8044
commit 3da97ffe2e
6 changed files with 0 additions and 3553 deletions
-46
View File
@@ -1,46 +0,0 @@
## Authentication
src/apps/officer-web/Screens/Authentication/ForgotPassword/useResetPassword.ts
src/apps/officer-web/Screens/Authentication/VerifyScreen/useVerifyScreen.ts
## Files
src/apps/officer-web/Screens/Dashboard/Files/state/usePinnedFiles.ts
src/apps/officer-web/Screens/Dashboard/Files/state/useRecentFiles.ts
## Officer-web State
src/apps/officer-web/state/useChatGroups.ts
src/apps/officer-web/state/useChatSessions.ts
src/apps/officer-web/state/useInitialData.ts
src/apps/officer-web/state/useLandingPage.ts
src/apps/officer-web/state/useModels.ts
src/apps/officer-web/state/usePlans.ts
src/apps/officer-web/state/useProjectsState.ts
src/apps/officer-web/state/useRecentModels.ts
src/apps/officer-web/state/useResources.ts
src/apps/officer-web/state/useServerSettings.ts
src/apps/officer-web/state/useSettings.ts
src/apps/officer-web/state/useThemeSync.ts
src/apps/officer-web/state/useUserState.ts
src/apps/officer-web/state/useWorkspacesState.ts
## Chat (apps/Chat)
src/workspaces/apps/Chat/useChatSessions.ts
src/workspaces/apps/Chat/useChatSession.ts
src/workspaces/apps/Chat/usePi.ts
src/workspaces/apps/Chat/useSlashCommands.ts
## Other Workspaces
src/workspaces/apps/CodeEditor/useEditorState.ts
src/workspaces/apps/FileBrowser/useFiles.ts
src/workspaces/apps/FileBrowser/useTasks.ts
src/workspaces/components/DataTable/useFixedHeightPagination.ts
src/workspaces/components/ui/hooks/use-mobile.tsx
src/workspaces/components/ui/hooks/use-toast.ts
src/workspaces/components/ui/use-toast.ts
src/workspaces/i18n/src/useTranslation.ts
src/workspaces/injector/use-client.ts
Done!
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-310
View File
@@ -1,310 +0,0 @@
# Officer Setup Flow Analysis
## Overview
The setup.sh script installs all dependencies for Officer, but there are several issues that can cause problems, especially with nvm Node.js environments.
## Setup Flow
```
1. Detect package manager (apt/pacman/brew)
2. Install core system packages (git, zip, curl, zsh, build-essential, etc.)
3. Install archive utilities (7z, unrar)
4. Install ffmpeg
5. Configure sudoers for service user
6. Install Node.js 22 (or warn if not found)
7. Configure npm global prefix (~/.npm-global)
8. Install Bun
9. Install Go 1.23.6
10. Install Rust
11. Install PulseAudio (for audio)
12. Build cliamp from source (Go music player)
13. Install Neovim
14. Install terminal tools (starship, oh-my-zsh, eza, lazygit)
15. Install yt-dlp (optional)
16. Install npm global packages (Pi, Claude Code, pm2)
17. Run bun install (project dependencies)
18. Setup remote desktop (XFCE + VNC)
19. Setup PTY sidecar (systemd service)
20. Verification
```
## Issues Found
### 1. **nvm Node.js Not Properly Documented**
**Location:** `scripts/setup.sh` (line ~238)
**Problem:**
```bash
if has node; then
NODE_VER=$(node -v 2>/dev/null | tr -d 'v')
NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
if [ "$NODE_MAJOR" = "22" ]; then
skip "node v$NODE_VER"
else
warn "Node $NODE_VER found but v22 is required"
warn "Use nvm: nvm install 22 && nvm use 22"
fi
else
warn "Node.js not found — install v22 via nvm:"
warn " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
warn " nvm install 22"
fi
```
**Issue:**
- Only warns about nvm but doesn't ensure it's sourced in the current shell
- When installing pm2 and other npm global packages, nvm might not be available
- When services (pm2, systemd) later run, they won't have nvm initialized
**Impact:**
- Users install nvm, run setup.sh in that shell, but when pm2/systemd runs later, it uses the wrong node or no node
---
### 2. **PTY Sidecar Service Uses Unbounded `which node`**
**Location:** `scripts/setup-pty-sidecar.sh` (line ~16)
**Problem:**
```bash
NODE_BIN="$(which node)"
```
**Issue:**
- If nvm is not sourced in the current shell, `which node` returns nothing or the system node
- The systemd service will run with the wrong node binary
- When systemd runs, nvm environment is not available anyway
**Impact:**
- PTY sidecar service fails to start or runs with wrong node
- Terminal functionality breaks in Officer
---
### 3. **PM2/Ecosystem Config Doesn't Handle nvm**
**Location:** `ecosystem.config.cjs`
**Problem:**
```javascript
module.exports = {
apps: [
{
name: 'officer',
script: 'bun',
args: 'start',
watch: false,
},
],
};
```
**Issue:**
- No environment setup for nvm
- PM2 runs with whatever node is in system PATH
- If user installed node via nvm, PM2 won't find it
- This is why you had to restart the server after installing nvm
**Impact:**
- Officer server fails to start after fresh nvm installation
- No clear error message about nvm not being available
---
### 4. **No Documentation on Node Installation Methods**
**Location:** `scripts/setup.sh` (lines 238-250)
**Problem:**
- Script warns about nvm but doesn't explain the workflow
- No mention of snap node incompatibility
- No mention of system apt/NodeSource installation
- No guidance on which method to use when
**Impact:**
- Users can choose any installation method
- Some methods (snap) don't work with Officer
- New issues arise from incompatible setups
---
### 5. **Pi Installation Doesn't Validate nvm Environment**
**Location:** `scripts/setup.sh` (lines ~408-420)
**Problem:**
```bash
if has pi; then
skip "pi (@mariozechner/pi-coding-agent)"
else
npm install -g @mariozechner/pi-coding-agent
if has pi; then ok "pi installed"; else warn "pi install failed"; fi
fi
```
**Issue:**
- Installs pi with `npm install -g`, but npm might be different than later shells
- No validation that pi works (should test `pi --list-models`)
- No check that Pi was installed to the right npm location
**Impact:**
- Pi appears installed but fails at runtime when shell environment differs
---
## Fixes Required
### Fix 1: Source nvm Before Installing Global Packages
```bash
# At the start of setup.sh, after detecting package manager
echo ""
echo "── Node.js Environment ──"
# Check if nvm needs to be sourced
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
nvm use 22 || nvm install 22
ok "nvm activated: $(node -v)"
elif ! has node; then
fail "Node.js not found and nvm not installed"
fail "Install nvm first: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
exit 1
fi
```
### Fix 2: Update PTY Sidecar Setup to Use Correct Node
```bash
# In scripts/setup-pty-sidecar.sh
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
nvm use 22 2>/dev/null || true
fi
NODE_BIN="$(which node)"
if [ ! -f "$NODE_BIN" ]; then
echo "ERROR: Node.js not found in PATH"
exit 1
fi
```
### Fix 3: Update PM2 Ecosystem Config
```javascript
module.exports = {
apps: [
{
name: 'officer',
script: 'bun',
args: 'start',
watch: false,
// Source nvm before running
exec_mode: 'cluster',
instances: 1,
env: {
NODE_ENV: 'production',
// This helps systemd find the right node
NVM_DIR: '$HOME/.nvm',
},
// For systemd service, use a wrapper script
},
],
};
```
Or better: Create a wrapper script for PM2:
```bash
#!/bin/bash
# bin/start.sh
set -euo pipefail
# Source nvm if available
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
fi
# Now start Officer
NODE_ENV=production bun src/server.tsx
```
Then in ecosystem.config.cjs:
```javascript
{
name: 'officer',
script: 'bin/start.sh',
// ...
}
```
### Fix 4: Validate Pi Installation
```bash
# After installing Pi
if has pi; then
if pi --list-models > /dev/null 2>&1; then
ok "pi installed and working"
else
warn "pi installed but --list-models failed"
warn "Try: nvm use && npm install -g @mariozechner/pi-coding-agent"
fi
else
warn "pi install failed"
fi
```
### Fix 5: Add Setup Documentation
Create `SETUP_GUIDE.md` with clear instructions on:
1. Choose ONE Node installation method (recommend nvm)
2. Source nvm in shell before running setup.sh
3. Setup.sh will validate node and npm are available
4. Services (PM2, systemd) will inherit nvm environment
---
## Recommendations
1. **Make nvm sourcing automatic** at the start of setup.sh
2. **Add environment wrapper script** for PM2 that sources nvm
3. **Document the three Node installation options** with pros/cons:
- nvm (recommended, flexible versions)
- NodeSource (system package, simple)
- apt (if available in repo)
- ❌ snap (broken, don't use)
4. **Validate Pi works** before marking setup complete
5. **Create a post-setup check script** that verifies everything works
---
## Current Workaround
If setup.sh already ran with snap node:
1. Remove snap: `sudo snap remove node`
2. Install nvm: `curl -o- ... | bash` (reload shell)
3. Install node: `nvm install 22 && nvm use 22`
4. Reinstall pm2 packages: `npm install -g pm2`
5. Restart pm2/officer
This is what you just did!
-92
View File
@@ -1,92 +0,0 @@
# Per-User API Keys — Architecture Analysis
## Current State
- Single shared `auth.json` at `~/.pi/agent/auth.json` (server-side)
- Pi CLI supports `--api-key` flag — we already use this in `pi-bridge.ts` via `resolveApiKeyForModel()`
- User settings have unused fields: `ai.enabledModels`, `ai.enabledProviders`, `ai.disabledProviders`
- 13 providers supported: anthropic, openai, google, groq, mistral, xai, openrouter, minimax, huggingface, azure-openai-responses, opencode, zai, cerebras
## Architecture
### Layer 1: Key Resolution
**Precedence:** User key > System key > fail
- Store user keys in `user_settings.ai.apiKeys` (JSONB column, no new DB table needed)
- Format: `{ "ai": { "apiKeys": { "openai": "sk-...", "anthropic": "sk-ant-..." } } }`
- `resolveApiKeyForModel()` in `pi-bridge.ts` checks user keys first, falls back to system `auth.json`
- Key passed to Pi via `--api-key` CLI flag (already implemented for system keys)
- Keys never touch the container filesystem — all resolution is server-side
### Layer 2: Model Visibility
- System access policy defines base available providers/models
- User's own provider keys unlock additional providers
- Existing `enabledProviders`/`enabledModels` settings fields drive filtering
- Model listing becomes a union: system models + user's provider models
## Key Design Decisions
1. **No new DB table** — use existing `user_settings` JSONB column
2. **No per-user auth.json files** — all key resolution server-side via `--api-key` flag
3. **Eventually remove PI_CONFIG_DIR mount** — auth.json in container no longer needed for keys (still needed for local providers/models.json)
4. **Model listing = union** of system + user models
## What This Enables
- Admin deploys with their keys → all users can chat
- User adds their own key → gets access to that provider
- User's key takes priority (user pays for their own usage)
- No credential leakage (keys in DB, passed as CLI args, never on container filesystem)
## Files to Change
| File | Change |
|------|--------|
| `src/servers/api/pi/pi-bridge.ts` | Update `resolveApiKeyForModel()` to check user settings first |
| `src/servers/api/settings/settings.ts` | API endpoint for saving/deleting user API keys (encrypted at rest ideally) |
| `src/workspaces/state/src/useSettings.ts` | Add `ai.apiKeys` to `UserSettings` type |
| `src/servers/api/pi/list-models.ts` | Union system + user provider models |
| `src/workspaces/officerdev/src/hooks/usePiModels.ts` | Filter models based on user's available providers |
| `src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/AIModels.tsx` | UI for managing per-user API keys |
| `src/servers/api/server-settings/pi-mono.ts` | Distinguish admin vs user key management |
## Deep-Dive: Pi CLI & Auth
### CLI Flags
- `--provider` — force provider
- `--model` — force model (format: `provider/model-name`)
- `--api-key` — force API key (takes precedence over auth.json and env vars)
- `--system-prompt` — system prompt
### Supported Environment Variables
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`, `MISTRAL_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY`, etc.
### auth.json Format
```json
{
"provider_id": {
"type": "api_key",
"key": "sk-..."
}
}
```
### Key Resolution Precedence (inside Pi)
`--api-key` flag > auth.json entry > environment variable > built-in providers
### Pi Spawn Flow (pi-bridge.ts)
1. Determine model from user settings or default
2. `resolveApiKeyForModel(model)` extracts provider from `provider/model-name`
3. Looks up key in auth.json (currently system-level only)
4. Passes `--api-key <key>` when spawning Pi process
5. Container never sees raw credentials
## Implementation Steps
1. **Add key storage** — extend `UserSettings` type, add API endpoint for CRUD
2. **Update key resolution**`resolveApiKeyForModel(model, userId?)` checks user DB first
3. **Update model listing** — merge system models with user-unlocked provider models
4. **Build settings UI** — API key input per provider in AI settings page
5. **Remove PI_CONFIG_DIR mount** (later) — once models.json is also handled server-side
-361
View File
@@ -1,361 +0,0 @@
# Event Handler Instances: `{() => handler(arg)}` Pattern
Found 167 instances across the codebase.
---
## Workspaces / Shared Components
### `src/workspaces/components/MarkdownEditor.tsx`
- Line 48: `onClick={() => setShowPreview(false)}`
- Line 52: `onClick={() => setShowPreview(true)}`
### `src/workspaces/components/SearchInput.tsx`
- Line 49: `onClick={() => handleSearch('')}`
### `src/workspaces/components/ErrorDialogs/CustomError.tsx`
- Line 25: `onClick={() => onOpenChange(false)}`
### `src/workspaces/components/ErrorDialogs/ForbiddenDialog.tsx`
- Line 27: `onClick={() => onOpenChange(false)}`
### `src/workspaces/components/ErrorDialogs/UnauthorizedDialog.tsx`
- Line 29: `onClick={() => onOpenChange(false)}`
### `src/workspaces/components/ErrorDialogs/ServerError.tsx`
- Line 25: `onClick={() => onOpenChange(false)}`
### `src/workspaces/components/DataTable/PaginationBar.tsx`
- Line 83: `onClick={() => changePage(1)}`
- Line 93: `onClick={() => changePage(currentPage - 1)}`
- Line 103: `onClick={() => changePage(currentPage + 1)}`
- Line 113: `onClick={() => changePage(pageCount)}`
### `src/workspaces/components/DataTable/DataTable.tsx`
- Line 89: `onClick={() => sortBy(sortKey as Extract<keyof T, string> | undefined)}`
### `src/workspaces/components/ui/mode-toggle.tsx`
- Line 16: `onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}`
---
## Editor App
### `src/apps/editor/app/src/EditorV2/index.tsx`
- Line 73: `onCollapse={() => setLeftCollapsed(true)}`
- Line 74: `onExpand={() => setLeftCollapsed(false)}`
- Line 97: `onCollapse={() => setRightCollapsed(true)}`
- Line 98: `onExpand={() => setRightCollapsed(false)}`
### `src/apps/editor/app/src/EditorV2/ScreenshotDialog.tsx`
- Line 114: `onClick={() => setView('list')}`
- Line 131: `onClick={() => setView('editor')}`
### `src/apps/editor/app/src/EditorV2/Header/index.tsx`
- Line 110: `onClick={() => setDevice(d)}`
- Line 134: `onClick={() => setDevice(d)}`
### `src/apps/editor/app/src/EditorV2/Header/GlobalCodeMenu.tsx`
- Line 32: `onClick={() => openEditor('js')}`
- Line 33: `onClick={() => openEditor('css')}`
### `src/apps/editor/app/src/EditorV2/Header/ElementSelector.tsx`
- Line 59: `onClick={() => setIsOpen(false)}`
### `src/apps/editor/app/src/EditorV2/RightSidebar/ElementInfo.tsx`
- Line 23: `setTimeout(() => setCopiedHierarchical(false), 2000)`
- Line 31: `setTimeout(() => setCopiedClassBased(false), 2000)`
### `src/apps/editor/app/src/EditorV2/RightSidebar/Breadcrumb.tsx`
- Line 60: `onClick={() => selectMember(index)}`
- Line 100: `onClick={() => selectMember(index)}`
### `src/apps/editor/app/src/EditorV2/RightSidebar/fields/PropertyColor.tsx`
- Line 29: `return () => clearInterval(interval)`
### `src/apps/editor/app/src/EditorV2/LeftSidebar/HierarchyTab.tsx`
- Line 25: `onClick={() => onSelect(index)}`
### `src/apps/editor/app/src/EditorV2/LeftSidebar/ChangesTab.tsx`
- Line 46: `onMouseEnter={() => handleMouseEnter(change)}`
- Line 48: `onClick={() => selectByChange(change)}`
### `src/apps/editor/app/src/EditorV2/Preview/index.tsx`
- Line 118: `onClick={() => setInteractive(!interactive)}`
- Line 132: `onClick={() => setMoveAnywhere(!moveAnywhere)}`
### `src/apps/editor/app/src/Editor/CodeEditorWindow/index.tsx`
- Line 31: `onClick={() => setIsFullscreen(false)}`
- Line 33: `onClick={() => setIsFullscreen(true)}`
### `src/apps/editor/app/src/Editor/ContextMenu/index.tsx`
- Line 20: `onClick={() => setIsContextOpen(false)}`
- Line 26: `onClick={() => openEditor('html')}`
### `src/apps/editor/app/src/Editor/BottomToolbar/GlobalCode/index.tsx`
- Line 22: `onClick={() => openEditor('js')}`
- Line 27: `onClick={() => openEditor('css')}`
### `src/apps/editor/app/src/Editor/BottomToolbar/VariantSelector/index.tsx`
- Line 44: `onClick={() => setIsOpen(true)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/VariantSelector/CreateNewVariant.tsx`
- Line 23: `onClose={() => setIsOpen(false)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/QuerySelector/index.tsx`
- Line 49: `onClick={() => setIsOpen(false)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/DeviceSelector/index.tsx`
- Line 23: `onSelect={() => setDevice(device)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/MoveAnywhere/index.tsx`
- Line 12: `onClick={() => setMoveAnywhere((curr) => !curr)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/InteractivitySelector/index.tsx`
- Line 13: `onClick={() => setInteractive((curr) => !curr)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/Changes/ChangesList.tsx`
- Line 26: `onMouseEnter={() => selectByChange(change)}`
- Line 48: `onClick={() => removeChange(change)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/Screenshot/ScreenshotList.tsx`
- Line 38: `onMouseEnter={() => setHovered(screenshot.id)}`
- Line 39: `onMouseLeave={() => setHovered(null)}`
- Line 56: `onClick={() => setEditing(null)}`
- Line 64: `onClick={() => setEditing(screenshot.id)}`
- Line 68: `onClick={() => setWantsToDelete(screenshot.id)}`
- Line 75: `onConfirm={() => onDeleteConfirm(screenshot.id)}`
- Line 76: `onClose={() => setWantsToDelete(null)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/Screenshot/Controls.tsx`
- Line 87: `onClick={() => handleUpload(comment)}`
### `src/apps/editor/app/src/Editor/BottomToolbar/Screenshot/index.tsx`
- Line 38: `onClick={() => setView('list')}`
- Line 47: `onClick={() => setView('upload')}`
### `src/apps/editor/extension/src/Auth/SigninPage.tsx`
- Line 18: `setTimeout(() => navigate('/'), 100)`
### `src/apps/editor/extension/src/Screens/ExperimentList/ExperimentVariantList/SearchFilter.tsx`
- Line 22: `onClick={() => onSearchChange('')}`
---
## Dashboard App
### `src/apps/dashboard/hooks/use-toast.tsx`
- Line 143: `const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })`
### `src/apps/dashboard/Screens/AdminTools/TestExperiments/TestExperiments.tsx`
- Line 38: `onClick={() => setShowDuplicateModal(true)}`
- Line 48: `onClose={() => setShowDuplicateModal(false)}`
### `src/apps/dashboard/Screens/Dashboard/Monitor/TrendTable.tsx`
- Line 99: `onToggle={() => toggleRow(`trend-${exp.id}`)}`
- Line 114: `onClick={() => setCurrentPage(1)}`
- Line 120: `onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}`
- Line 128: `onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}`
- Line 136: `onClick={() => setCurrentPage(totalPages)}`
### `src/apps/dashboard/Screens/Dashboard/Monitor/GroupedExperimentsTable.tsx`
- Line 75: `onToggle={() => toggleRow(`grouped-${exp.id}`)}`
- Line 91: `onClick={() => setCurrentPage(1)}`
- Line 97: `onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}`
- Line 105: `onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}`
- Line 113: `onClick={() => setCurrentPage(totalPages)}`
### `src/apps/dashboard/Screens/Dashboard/Home/ExperimentsOverviewTableMobile.tsx`
- Line 23: `onClick={() => sort('id')}`
- Line 28: `onClick={() => sort('status')}`
- Line 34: `onClick={() => sort('sessions')}`
### `src/apps/dashboard/Screens/Dashboard/Home/ConfirmDialogs.tsx`
- Line 36: `onOpenChange={() => setDialogAction(null)}`
### `src/apps/dashboard/Screens/Dashboard/Home/RowActions.tsx`
- Line 31: `onClick={() => navigate(`/experiments/${id}`)}`
- Line 32: `onClick={() => setDialogAction('duplicate')}`
- Line 34: `onClick={() => setDialogAction('archive')}`
- Line 37: `onClick={() => setDialogAction('delete')}`
### `src/apps/dashboard/Screens/Dashboard/Clients/index.tsx`
- Line 76: `onClick={() => setIsCreating(true)}`
- Line 108: `onClick={() => setIsCreating(true)}`
- Line 119: `onClick={() => setOrganization(value as number)}`
- Line 146: `onClick={() => handleSaveEdit(item.id)}`
- Line 163: `onClick={() => handleEditName(item.id, item.friendlyName ?? '')}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/Timeline.tsx`
- Line 111: `onClick={() => setMobileOpen((v) => !v)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Library/ActionModals.tsx`
- Line 37: `onCancel={() => setDialogAction(null)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Library/TopHeader.tsx`
- Line 44: `onClick={() => setIsCreating(true)}`
- Line 69: `onClick={() => setIsCreating(true)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Library/ActionsCell.tsx`
- Line 29: `onClick={() => navigate(`/experiments/${id}`)}`
- Line 30: `onClick={() => setDialogAction('duplicate')}`
- Line 32: `onClick={() => setDialogAction('archive')}`
- Line 35: `onClick={() => setDialogAction('delete')}`
- Line 44: `onClose={() => setDialogAction(null)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Library/CreateExperimentModal.tsx`
- Line 88: `onCancel={() => setIsCreating(false)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/Header.tsx`
- Line 86: `onClick={() => setDialogAction('duplicate')}`
- Line 89: `onClick={() => setDialogAction('archive')}`
- Line 92: `onClick={() => setDialogAction('delete')}`
- Line 199: `onCancel={() => setDialogAction(null)}`
- Line 214: `onCancel={() => setDialogAction(null)}`
- Line 232: `onCancel={() => setDialogAction(null)}`
- Line 246: `onCancel={() => setDialogAction(null)}`
### `src/apps/dashboard/Screens/Dashboard/Settings/Billing/index.tsx`
- Line 137: `onClick={() => createCheckoutSession(+price.id)}`
### `src/apps/dashboard/Screens/Dashboard/Settings/AgencyTeam/index.tsx`
- Line 125: `onClick={() => setInviteDialogOpen(true)}`
- Line 136: `onClick={() => setInviteDialogOpen(true)}`
- Line 176: `onClick={() => resendInvite(item.email)}`
- Line 217: `() => unblockUser(item.id)`
- Line 230: `() => blockUser(item.id)`
- Line 239: `onClick={() => resendInvite(item.email)}`
- Line 247: `() => deleteUser(item.id)`
- Line 303: `onClick={() => setInviteDialogOpen(false)}`
- Line 315: `onCancel={() => setConfirmDialog({ ...confirmDialog, open: false })}`
### `src/apps/dashboard/Screens/Dashboard/Settings/OrganizationTeam/index.tsx`
- Line 163: `onClick={() => setInviteDialogOpen(true)}`
- Line 176: `onClick={() => setInviteDialogOpen(true)}`
- Line 217: `onClick={() => resendInvite(item.email)}`
- Line 259: `() => unblockUser(item.id)`
- Line 272: `() => blockUser(item.id)`
- Line 281: `onClick={() => resendInvite(item.email)}`
- Line 289: `() => deleteUser(item.id)`
- Line 361: `onClick={() => setInviteDialogOpen(false)}`
- Line 373: `onCancel={() => setConfirmDialog({ ...confirmDialog, open: false })}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/VariantsConfig.tsx`
- Line 57: `onClick={() => setShowAddVariant(true)}`
- Line 167: `onClose={() => setEditingVariant(null)}`
- Line 176: `onClose={() => setRenamingVariant(null)}`
- Line 180: `onClose={() => setShowAddVariant(false)}`
- Line 193: `onCancel={() => setDeletingVariant(null)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/CookieTargeting.tsx`
- Line 50: `onClick={() => deleteCookieTargeting(item.id)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/DeviceTargeting.tsx`
- Line 54: `onClick={() => deleteDeviceTargeting(device.id)}`
- Line 65: `onClick={() => setIsAddOpen(true)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/GoalsConfig.tsx`
- Line 85: `onCancel={() => setIsDialogOpen(false)}`
- Line 97: `onCheckedChange={() => toggleGoal(tag.name)}`
- Line 113: `onCheckedChange={() => toggleGoal(tag.name)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/UrlTargeting.tsx`
- Line 95: `onClick={() => openEditDialog(item)}`
- Line 98: `onClick={() => deleteUrlTargeting(item.id)}`
- Line 119: `onCancel={() => setEditItem(null)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentSettings/EditWeightsModal.tsx`
- Line 25: `useState(() => String(Math.round((currentWeight || 0) / 100)))`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentInfo/Description.tsx`
- Line 44: `onClick={() => setIsEditing(true)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentStatistics/StatisticsTable.tsx`
- Line 209: `onClick={() => setWantsToDeploy(item)}`
- Line 227: `onCancel={() => setWantsToDeploy(null)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentInfo/Screenshots/ScreenshotItem.tsx`
- Line 62: `onClick={() => setIsOpen(true)}`
- Line 65: `onLoad={() => setImageLoaded(true)}`
- Line 90: `onClick={() => setEditing(null)}`
- Line 107: `onClick={() => setWantsToDelete(true)}`
- Line 114: `onClose={() => setIsOpen(false)}`
- Line 119: `onCancel={() => setWantsToDelete(false)}`
### `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentInfo/Screenshots/ScreenshotList.tsx`
- Line 49: `onMouseEnter={() => setHovered(screenshot.id)}`
- Line 49: `onMouseLeave={() => setHovered(null)}`
### `src/apps/dashboard/Screens/Dashboard/Websites/List/index.tsx`
- Line 71: `onClick={() => setIsCreating(true)}`
- Line 100: `onClick={() => setIsCreating(true)}`
- Line 112: `onClick={() => setWebsite(value as number)}`
- Line 167: `onClick={() => refreshPropertyTags(item)}`
- Line 190: `onClick={() => recheckPermissions(item.ganPropertyId!)}`
- Line 220: `onClick={() => handleDelete(value as number)}`
### `src/apps/dashboard/Screens/Dashboard/Websites/List/ServerContainerUrl.tsx`
- Line 18: `setTimeout(() => setNewServerContainerUrl(null), 400)`
- Line 45: `onClick={() => setNewServerContainerUrl(null)}`
- Line 53: `onClick={() => setNewServerContainerUrl(website.serverContainerUrl || '')}`
### `src/apps/dashboard/Screens/Dashboard/Websites/List/ScriptsModal.tsx`
- Line 18: `setTimeout(() => setCopiedText(null), 3000)`
- Line 23: `onClick={() => setIsOpen(true)}`
- Line 74: `onClick={() => setIsOpen(false)}`
### `src/apps/dashboard/Screens/Dashboard/Websites/GoogleAnalytics/GoogleAnalyticsScreen.tsx`
- Line 91: `onClick={() => refresh(item.email)}`
### `src/apps/dashboard/Screens/Dashboard/DashboardLayout/index.tsx`
- Line 17: `useGlobal<string>('PAGE_TITLE', () => getPageTitle(location.pathname, MENU_LINKS))`
### `src/apps/dashboard/Screens/Dashboard/DashboardLayout/GlobalAlerts/index.tsx`
- Line 14: `onClose={() => dismiss('Extension')}`
- Line 15: `onClose={() => dismiss('Passkeys')}`
### `src/apps/dashboard/Screens/Dashboard/DashboardLayout/AppSidebar/Navigation.tsx`
- Line 61: `onClick={() => setExpandedMenuItem(expandedMenuItem === item.key ? '' : item.key)}`
### `src/apps/dashboard/Screens/Dashboard/DashboardLayout/AppSidebar/AccountWebsiteSelectors.tsx`
- Line 42: `onClick={() => setOpenPanel('account')}`
- Line 55: `onClick={() => setOpenPanel('website')}`
- Line 315: `onClick={() => chooseOptionValue(option.value as number)}`
---
## Summary by Pattern Type
### 1. Boolean State Setters (e.g., `setState(true/false)`)
Most common pattern - 60+ instances
### 2. Value State Setters (e.g., `setState(someValue)`)
~40 instances
### 3. Handlers with Arguments (e.g., `handleClick(id)`)
~35 instances
### 4. Callbacks to null (e.g., `setItem(null)`)
~25 instances
### 5. Navigation (e.g., `navigate(path)`)
~5 instances
### 6. setTimeout callbacks
~5 instances
---
## High Priority (Inside loops/maps)
These create N function instances per render:
- `src/apps/editor/app/src/Editor/BottomToolbar/Screenshot/ScreenshotList.tsx:38-76`
- `src/apps/editor/app/src/EditorV2/LeftSidebar/ChangesTab.tsx:46-48`
- `src/apps/editor/app/src/EditorV2/RightSidebar/Breadcrumb.tsx:60,100`
- `src/apps/editor/app/src/EditorV2/LeftSidebar/HierarchyTab.tsx:25`
- `src/apps/dashboard/Screens/Dashboard/Experiments/Details/ExperimentInfo/Screenshots/ScreenshotList.tsx:49`
- `src/apps/dashboard/Screens/Dashboard/Settings/AgencyTeam/index.tsx` (multiple in table rows)
- `src/apps/dashboard/Screens/Dashboard/Settings/OrganizationTeam/index.tsx` (multiple in table rows)
- `src/apps/dashboard/Screens/Dashboard/Websites/List/index.tsx` (multiple in table rows)
- `src/apps/dashboard/Screens/Dashboard/Clients/index.tsx` (multiple in table rows)