# Lessons Archive (1-70)

> These lessons were originally in CLAUDE.md. They've been archived because the fixes are now baked into the codebase. Consult this file if you encounter a regression in any of these areas.

**1. DateTime Handling — Always timezone-aware:** `Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))`. NEVER use `datetime.utcnow()`.

**2. SQLAlchemy Async — No lazy loading on new objects:** Use direct SQL for junction tables: `await db.execute(tree_tag_assignments.insert().values(...))`.

**3. React State — Don't store object snapshots:** Store IDs, derive objects: `const editingNode = editingNodeId ? findNode(editingNodeId, tree?.tree_structure) : null`.

**4. Modal Draft State — Exclude store-managed fields:** `const { children, ...draftWithoutChildren } = draft; updateNode(node.id, draftWithoutChildren)`.

**5. Multiple FKs to same table:** Specify `foreign_keys` on BOTH sides: `author = relationship("User", foreign_keys=[author_id], back_populates="trees")`.

**6. PostgreSQL NULL in UUID columns:** `SELECT 'tag', 'slug', NULL::uuid as team_id` — must cast NULL to uuid.

**7. API Path Gotcha:** Frontend `apiClient` baseURL is `http://localhost:8000/api/v1` — use relative paths WITHOUT `/api/v1/` prefix. Invite codes endpoint is `/invites` (NOT `/invite-codes`).

**8. CORS errors can mask 500s:** Check backend logs first. Also run `alembic upgrade head` after pulling changes.

**9. Public endpoints with optional auth:** Use manual `_get_optional_user(request, db)` helper, NOT `Optional[User]` param.

**10. React Router — Clear dirty state before navigation:** `markSaved()` before `navigate()`.

**11. CORS `expose_headers` for custom response headers:** Must be set in BOTH CORS branches in `main.py`.

**12. TreeStructure vs Tree types:** `TreeStructure` is for node structure only — it does NOT have `tree_type`, `name`, etc. Those are on `Tree`.

**13. Login redirect state format:** `navigate('/login', { state: { from: { pathname: '/path' } } })` — expects `state.from.pathname` (object), NOT a plain string.

**14. Type-aware routing:** Always use `getTreeNavigatePath(treeId, treeType)` from `@/lib/routing`. Procedural flows use `/flows/:id/navigate`.

**15. Session sharing types:** Use `tree_snapshot?.tree_type` to determine flow type from session data.

**16. tree_type has three values:** `'troubleshooting' | 'procedural' | 'maintenance'`. Use `PROCEDURAL_TREE_TYPES` set when checking for step-based flow types.

**17. Alembic autogenerate can be destructive:** Always review generated migrations. Prefer manual migrations for new tables.

**18. Pydantic partial updates — use `model_fields_set`:** Check `data.model_fields_set` to distinguish "field not sent" from "field sent as null".

**19. `gh pr merge` fails with worktrees:** Use the API: `gh api repos/ORG/REPO/pulls/N/merge --method PUT --field merge_method=squash`.

**20. `'answer'` node type in TreeStructure:** Transient stub type used only in the canvas editor. Must explicitly handle in any `node.type` switch.

**21. Test fixtures in `conftest.py`:** `client`, `test_db`, `test_user`, `auth_headers`, `test_tree`, `test_admin`, `admin_auth_headers`. NO `async_client` or `engineer_token`.

**22. Worktree venv path:** Use `backend/venv/bin/python -m pytest ...` from the worktree directory.

**23. Action nodes navigate via `next_node_id`, not `children`:** Must set `next_node_id` on action nodes for AI generation or manual editing.

**24. Anthropic model IDs:** Both alias (`claude-sonnet-4-6`) and dated forms work. Default: `claude-sonnet-4-6`.

**25. Claude API may wrap JSON responses in markdown fences:** Always strip fences before parsing. See `_strip_markdown_fences()`.

**26. `sessionsApi.list` supports `batch_id` filter.**

**27. Maintenance batch sessions are created all-at-once at launch.**

**28. AI tests in CI need `ai_enabled` mock:** Mock `settings.ai_enabled = True` via `PropertyMock`. See `tests/test_ai_chat.py`.

**29. ESLint `no-unused-vars` has no `argsIgnorePattern`:** Use `// eslint-disable-next-line` or remove the param.

**30. Alembic `env.py` must import all models.**

**31. JSONB fields — convert datetime to `.isoformat()` string.**

**32. Export pipeline order matters:** Generate → resolve variables → apply redaction. Redaction MUST run last.

**33. Railway: `DATABASE_URL_SYNC` is a property, not an env var.**

**34. Railway: run migrations in Docker CMD, not `releaseCommand`.**

**35. `bcrypt==4.0.1` pinned for passlib compatibility.**

**36. Email validator rejects `.local` TLD:** Use `example.com` for test/seed emails.

**37. First deployed user needs manual admin promotion via SQL.**

**38. Alembic migrations MUST use sequential numbered prefixes:** Always pass `--rev-id` flag.

**39. Platform settings for feature toggles:** Use `SettingsManager.get("key", db, default=True)`.

**40. Survey public routes:** Add at top level in `router.tsx` alongside `/login`.

---

## Archived Lessons (41-70)

**41. Assistant chat uses local React state, not Zustand:** `AssistantChatPage.tsx` uses `useState` for `chats`, `messages`, `input`, `loading`. No store.

**42. Public pages use raw `fetch()`, not `apiClient`:** Survey, shared sessions, and no-auth pages use `fetch()` with full URL. `apiClient` requires auth tokens.

**43. Adding new email types:** Add static async method to `EmailService` in `core/email.py`. Fire-and-forget from endpoints (log errors, don't fail).

**44. AI Chat Builder is flow-type-aware:** `ai_chat_service.py` dispatches by `flow_type`. Troubleshooting: `[TREE_UPDATE]` markers. Procedural: `[STEPS_UPDATE]` markers. Both support `[METADATA]`.

**45. Intake form field schema:** Uses `variable_name` and `field_type` (NOT `name` and `type`).

**46. `CreateFlowDropdown` uses `AIPromptDialog`:** Opens prompt modal, starts AI session, generates flow, navigates to editor with `{ state: { aiPanelOpen: true, sessionId } }`.

**47. Editor-Embedded Flow Assist:** `EditorAIPanel` (320px side panel) + `useEditorAI` hook. Ghost nodes use `_suggestion: true` flag. Delta responses use `[DELTA]...[/DELTA]` markers.

**48. Tree orphan validation uses dynamic root ID:** Orphan check compares against `state.treeStructure?.id` (NOT hardcoded `'root'`).

**49. Full-stack features — verify both ends:** schema → endpoint → API client → hook → store → UI.

**50. Anthropic SDK retry:** Set `max_retries=1` to fail fast. Default `max_retries=2` can take 3× timeout.

**51. AI model tier routing:** Use `settings.get_model_for_action(action_type)`. Model IDs: alias form (`claude-sonnet-4-6`).

**52. Mobile scroll-to-top:** Use `ref.current.scrollIntoView()`, not `window.scrollTo()`. Trigger via `useEffect`.

**53. Flex height chain:** Every ancestor must be a flex container for `flex-1` to work. Missing `flex` class collapses React Flow to 0 height.

**54. React Flow CSS in Tailwind v4:** Import in `index.css`, not component JS. Override dark theme using `--xy-*` CSS custom properties.

**55. App shell height chain:** Every wrapper between `.main-content` and canvas needs `flex` + `flex-1` + `min-h-0` or `h-full`.

**56. Railway backend service name is `patherly`:** Production DB name is `railway`. Public Postgres proxy: `interchange.proxy.rlwy.net:45797`.

**57. Node field priority:** `title` → `question` → `description` → `content` → `label`. See `copilot_service.py`.

**58. `scriptGeneratorStore.generate()` optional param:** Always wrap: `onClick={() => generate()}`, never `onClick={generate}`.

**59. ConnectWise `clientId` is server-side config:** Set in `config.py` as `CW_CLIENT_ID`. Per-connection: `company_id`, `public_key`, `private_key`, `server_url`.

**60. Dockerfile build args for Vite env vars:** Any new `VITE_*` var must be added as `ARG` + `ENV` in `frontend/Dockerfile`. Railway env vars are runtime-only without this; `import.meta.env.VITE_*` resolves to `undefined` in production builds.

**61. Procedural sessions auto-start on page load:** `ProceduralNavigationPage` calls `startSession()` immediately in `loadTree()` — no intake form screen or "Start" button. Variables filled inline. Troubleshooting flows DO have a start screen.

**62. Playwright strict mode — scope selectors:** Step titles appear in both sidebar and main heading. Use `getByRole('heading', { name })` for main content.

**63. Node 20 required for frontend builds:** `export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20`. Or: `PATH="$HOME/.nvm/versions/node/v20.19.0/bin:$PATH"`.

**64. PostHog product analytics:** `PostHogProvider` in `main.tsx`. Event helpers in `lib/analytics.ts`. `identifyUser()` in `authStore.fetchUser()`, `resetAnalytics()` on logout. Env vars: `VITE_PUBLIC_POSTHOG_KEY`, `VITE_PUBLIC_POSTHOG_HOST`.

**65. Local Docker Compose uses `resolutionflow` database on port 5433:** Container `resolutionflow_postgres`, DB `resolutionflow` (not `patherly`), port `5433`. Playwright config defaults must match.

**66. Dev environment runs on Hostinger VPS (46.202.92.250):** CORS must include VPS IP in `CORS_ORIGINS` and `FRONTEND_URL`. See DEV-ENV.md.

**67. Tree editor route is `/trees/new`:** NOT `/editor/new`. Use `getTreeEditorPath()` from `@/lib/routing`.

**68. APScheduler jobs need `max_instances=1`:** Without it, overlapping runs can process the same records twice (TOCTOU race).

**69. PostgreSQL `func.sum(case(...))` returns `Decimal` via asyncpg:** Cast to `int()` before storing in Pydantic `dict[str, Any]` fields.

**70. Toast library uses `toast.warning()` not `toast.warn()`:** Import from `@/lib/toast`. Methods: `success`, `error`, `warning`, `info`.
