Files
resolutionflow/LESSONS-LEARNED.md
Michael Chihlas 2421f10dbd Complete rebrand from Apoklisis to Patherly
- Update all frontend branding (title, headers, login/register pages)
- Update documentation (CLAUDE-SETUP, CURRENT-STATE, PROGRESS, LESSONS-LEARNED)
- Update backend scripts and test configuration
- Fix emoji encoding in seed scripts for Windows compatibility
- Sync seed user credentials between seed_data.py and seed_trees.py
- Update database references to patherly/patherly_test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:55:55 -05:00

580 lines
17 KiB
Markdown

# Lessons Learned
> **Purpose:** This file documents bugs, fixes, and gotchas encountered during development.
> **For Claude Code:** Read this file at the start of each session to avoid repeating past mistakes.
> **Last Updated:** January 30, 2026
---
## Environment Setup (New Machine)
### Database Name Mismatch After Fresh Clone
**Problem:** After cloning the repo and running `docker-compose up -d`, Alembic migrations fail with `database "decision_tree" does not exist`.
**Cause:** The `.env` file contains the old database name (`decision_tree`) but `docker-compose.yml` creates a database called `patherly`.
**Solution:** Update `.env` to use the correct database name:
```
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/patherly
DATABASE_URL_SYNC=postgresql://postgres:postgres@localhost:5432/patherly
```
**Files affected:** `backend/.env`
---
### Missing @/lib/utils (cn function)
**Problem:** Frontend fails to compile with error: `Failed to resolve import "@/lib/utils" from "src/pages/SessionDetailPage.tsx". Does the file exist?`
**Cause:** The `src/lib/utils.ts` file wasn't committed to the repo or was missed during setup. This file provides the `cn()` utility function used for combining Tailwind classes.
**Solution:** Create `frontend/src/lib/utils.ts`:
```typescript
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
**Dependencies required:** `clsx` and `tailwind-merge` (already in package.json)
**Files affected:** `frontend/src/lib/utils.ts`
---
### pip install in venv doesn't need --break-system-packages
**Problem:** Confusion about whether to use `--break-system-packages` flag.
**Clarification:** The `--break-system-packages` flag is only needed when installing packages **outside** of a virtual environment. When your venv is active (you see `(venv)` prefix), you don't need this flag.
---
### New Machine Setup Checklist
When setting up development on a new machine:
1. **Clone repo:** `git clone <repo-url>`
2. **Start Docker Desktop**
3. **Start database:** `cd backend && docker-compose up -d`
4. **Fix .env database name** if it says `decision_tree` → change to `patherly`
5. **Create venv:** `python -m venv venv`
6. **Activate venv:** `.\venv\Scripts\Activate`
7. **Install backend deps:** `pip install -r requirements.txt`
8. **Run migrations:** `alembic upgrade head`
9. **Start backend:** `uvicorn app.main:app --reload`
10. **Install frontend deps:** `cd ../frontend && npm install`
11. **Create lib/utils.ts** if missing (see above)
12. **Start frontend:** `npm run dev`
---
## Python / Backend
### DateTime Timezone Handling ⚠️ CRITICAL
**Problem:** SQLAlchemy `DateTime` fields caused Internal Server Errors when mixing timezone-aware and timezone-naive datetimes.
**Error:** `can't subtract offset-naive and offset-aware datetimes`
**Solution:**
- Always use `DateTime(timezone=True)` in SQLAlchemy models
- Always use `datetime.now(timezone.utc)` for defaults and assignments
- Never use `datetime.utcnow()` (deprecated and timezone-naive)
**Correct pattern:**
```python
from datetime import datetime, timezone
from sqlalchemy import DateTime
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
```
**Files affected:** All models with timestamp fields (user.py, tree.py, session.py, team.py, attachment.py)
---
### pytest-asyncio Version Compatibility
**Problem:** Tests fail with `AttributeError: 'Package' object has no attribute 'obj'`
**Cause:** Incompatibility between pytest 8.0.0 and pytest-asyncio 0.23.3
**Solution:** Upgrade pytest-asyncio to 0.24.0
```powershell
pip install pytest-asyncio==0.24.0 --upgrade
```
---
### bcrypt / passlib Compatibility
**Problem:** Password hashing fails with newer bcrypt versions.
**Solution:** Pin bcrypt version in requirements.txt:
```
bcrypt==4.0.1
passlib[bcrypt]==1.7.4
```
---
### Virtual Environment Best Practices
**Problem:** OneDrive sync causes conflicts with venv, `__pycache__`, and lock files.
**Solution:**
- Keep project in `C:\Dev\Projects\`, NOT in OneDrive-synced folders
- Add to `.gitignore`: `venv/`, `__pycache__/`, `*.pyc`
---
### Installing Packages in venv
**Problem:** `pip install` sometimes installs globally instead of in venv.
**Solution:**
- Always verify venv is active: look for `(venv)` prefix in terminal
- If VS Code prompts to create a new venv when one exists, click "Don't show again"
- Use `pip install --break-system-packages` flag if needed (Python 3.12+)
---
### Alembic Migrations
**Problem:** Model changes not reflected in database.
**Solution:** Always run migrations after model changes:
```powershell
cd backend
alembic revision --autogenerate -m "Description of change"
alembic upgrade head
```
---
## TypeScript / Frontend
### React State: Don't Store Object Snapshots for Editing ⚠️ CRITICAL
**Problem:** Form inputs don't update when typing - characters appear then immediately disappear.
**Cause:** Storing a full object from a Zustand store in local state creates a snapshot. When the store updates, the local state still holds the old reference, causing the input's `value` to revert.
**Broken pattern:**
```tsx
// BAD: Stores a snapshot that won't update
const [editingNode, setEditingNode] = useState<TreeStructure | null>(null)
// When modal opens:
setEditingNode(node) // snapshot captured here
// In modal form:
<input value={editingNode.question} onChange={...} /> // always shows old value
```
**Solution:** Store only the ID, then fetch the current object from the store on each render:
```tsx
// GOOD: Store only the ID
const [editingNodeId, setEditingNodeId] = useState<string | null>(null)
const editingNode = editingNodeId ? findNode(editingNodeId) : null
// When modal opens:
setEditingNodeId(node.id)
// In modal form - now gets fresh data each render:
<input value={editingNode.question} onChange={...} />
```
**Why it works:** By calling `findNode()` on each render, the component always gets the current state from the store after updates.
**Files affected:** Any component that opens a modal/form to edit store data (NodeList.tsx)
---
### Modal Draft State: Don't Overwrite Store-Managed Fields ⚠️ CRITICAL
**Problem:** Child nodes created while a modal is open disappear when clicking "Done". Tree validation fails with errors about non-existent nodes.
**Cause:** When using local draft state for Cancel/Done functionality in a modal:
1. Modal opens and clones the entire node (including `children: []`) into local state
2. User creates new child nodes via a dropdown (e.g., NodePicker) - these are added to the **store**
3. User clicks "Done" → modal saves draft back to store
4. The draft's stale `children: []` **overwrites** the store's actual children array
5. Newly created nodes are deleted
**Symptoms:**
- Child nodes don't appear in the tree after closing modal
- Validation errors: "Option references non-existent node"
- Tree won't save due to validation failures
**Broken pattern:**
```tsx
// Modal with local draft state
const [draft, setDraft] = useState(() => structuredClone(node))
const handleSave = () => {
// BAD: This overwrites children with stale snapshot!
updateNode(node.id, draft)
onClose()
}
```
**Solution:** Exclude fields that are managed by other store actions (like `children` managed by `addNode`/`deleteNode`):
```tsx
const handleSave = () => {
// GOOD: Exclude 'children' - it's managed separately by addNode/deleteNode
const { children, ...draftWithoutChildren } = draft
updateNode(node.id, draftWithoutChildren)
onClose()
}
```
**General rule:** When a modal uses local draft state, identify which fields are:
- **Editable by the form** → include in draft save
- **Managed by other actions** (addNode, deleteNode, etc.) → exclude from draft save
**Files affected:** `NodeEditorModal.tsx`, any modal that edits objects with nested collections
---
### Modal Scroll/Overflow with Fixed Header and Footer
**Problem:** Modal content extends beyond screen when there's too much content, pushing close button and action buttons off-screen.
**Solution:** Use flex layout with fixed header/footer and scrollable body:
```tsx
// Modal structure
<div className="flex max-h-[85vh] w-full flex-col">
{/* Header - fixed at top */}
<div className="flex-shrink-0 border-b px-6 py-4">
<h2>{title}</h2>
<button>X</button>
</div>
{/* Body - scrollable */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{children}
</div>
{/* Footer - fixed at bottom */}
{footer && (
<div className="flex-shrink-0 border-t px-6 py-4">
{footer}
</div>
)}
</div>
```
**Key points:**
- `max-h-[85vh]` constrains total modal height
- `flex-col` enables vertical flex layout
- `flex-shrink-0` on header/footer prevents them from shrinking
- `flex-1 overflow-y-auto` on body makes it fill remaining space and scroll
**Files affected:** Modal.tsx, any component using modals for forms
---
### Lucide React Icons: No Title Prop
**Problem:** TypeScript error when trying to add `title` prop to Lucide icons.
**Error:** `Property 'title' does not exist on type 'LucideProps'`
**Broken pattern:**
```tsx
<CheckCircle title="Has solution endpoint" /> // ❌ Error
```
**Solution:** Wrap the icon in a span with the title:
```tsx
<span title="Has solution endpoint">
<CheckCircle className="h-4 w-4" />
</span>
```
---
### Tree Traversal: Preventing Infinite Loops with Visited Set
**Problem:** When traversing a tree structure that has cross-references (like `next_node_id` pointing to nodes elsewhere in the tree), you can get infinite loops.
**Solution:** Use a `visited` Set to track already-processed nodes:
```tsx
function hasSolutionInSubtree(
node: TreeStructure,
findNode: (id: string) => TreeStructure | null,
visited: Set<string> = new Set()
): boolean {
// Prevent infinite loops
if (visited.has(node.id)) return false
visited.add(node.id)
if (node.type === 'solution') return true
// Check children array
if (node.children) {
for (const child of node.children) {
if (hasSolutionInSubtree(child, findNode, visited)) return true
}
}
// Check next_node_id reference (could point anywhere in tree)
if (node.next_node_id) {
const nextNode = findNode(node.next_node_id)
if (nextNode && hasSolutionInSubtree(nextNode, findNode, visited)) {
return true
}
}
return false
}
```
**Why it matters:** Decision trees can have shared nodes where multiple paths converge on the same target. Without loop detection, recursive traversal will hang.
---
### SharedLinksMap Pattern for Tracking Cross-References
**Problem:** Need to know which nodes link to the same target node (for showing "shared by X nodes" indicators).
**Solution:** Build a map at the parent component level, pass down to children:
```tsx
// Type definition
type SharedLinksMap = Map<string, Array<{ id: string; label: string }>>
// Build the map by traversing tree once
function buildSharedLinksMap(
node: TreeStructure,
map: SharedLinksMap = new Map()
): SharedLinksMap {
const nodeLabel = node.type === 'decision' ? node.question : node.title
// Record decision option targets
if (node.type === 'decision' && node.options) {
for (const opt of node.options) {
if (opt.next_node_id) {
const existing = map.get(opt.next_node_id) || []
existing.push({ id: node.id, label: nodeLabel || 'Untitled' })
map.set(opt.next_node_id, existing)
}
}
}
// Record action next_node_id targets
if (node.type === 'action' && node.next_node_id) {
const existing = map.get(node.next_node_id) || []
existing.push({ id: node.id, label: nodeLabel || 'Untitled' })
map.set(node.next_node_id, existing)
}
// Recurse
if (node.children) {
for (const child of node.children) {
buildSharedLinksMap(child, map)
}
}
return map
}
// Use in parent with useMemo
const sharedLinksMap = useMemo(() => {
if (!treeStructure) return new Map()
return buildSharedLinksMap(treeStructure)
}, [treeStructure])
```
**Usage in child:** Check `sharedLinksMap.get(node.id)?.length > 1` to see if node is shared.
---
### tsconfig.json Strict Mode
**Problem:** VS Code shows warnings about missing compiler options.
**Solution:** Add to `compilerOptions` in tsconfig.json:
```json
{
"compilerOptions": {
"strict": true,
"forceConsistentCasingInFileNames": true
}
}
```
**Why it matters:** `forceConsistentCasingInFileNames` prevents issues when deploying from Windows (case-insensitive) to Linux (case-sensitive).
---
### Tailwind Dark Mode
**Pattern:** Use Tailwind's `dark:` variant for dark mode styling.
**Setup:** In `tailwind.config.js`:
```javascript
module.exports = {
darkMode: 'class', // or 'media' for system preference only
// ...
}
```
**Usage:**
```jsx
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
```
---
## Docker / PostgreSQL
### Accessing PostgreSQL Without Local psql
**Problem:** `psql` command not recognized on Windows (not installed locally).
**Solution:** Use Docker exec to run psql inside the container:
```powershell
# Single command
docker exec -it patherly_postgres psql -U postgres -c "SELECT * FROM users;"
# Interactive session
docker exec -it patherly_postgres psql -U postgres
# Create database
docker exec -it patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_test;"
```
---
### Docker Container Not Running
**Problem:** Database connection errors.
**Solution:** Check and start the container:
```powershell
docker ps # See running containers
docker start patherly_postgres # Start if stopped
```
---
## Git / Version Control
### git add from Wrong Directory
**Problem:** `git add .` doesn't stage files in parent directories.
**Solution:** Always run git commands from project root:
```powershell
cd C:\Dev\Projects\patherly
git add .
git commit -m "Your message"
git push origin main
```
---
### Untracked .claude/ Folder
**Problem:** `.claude/` folder appears in untracked files.
**Solution:** Either:
1. Add to `.gitignore` if you don't want to track it
2. Or `git add .claude/` if you want Claude Code settings in repo
---
## Environment-Specific Notes
### Windows Path Handling
- Python and Node handle forward slashes `/` fine on Windows
- Use `os.path.join()` or `pathlib.Path` for cross-platform compatibility
- Avoid hardcoding backslashes `\` in code
### PowerShell vs CMD
- Some Node.js/Python tools work better in CMD than PowerShell
- If a command fails in PowerShell, try CMD or add to Desktop Commander config:
- `defaultShell: "cmd"`
---
## API / Endpoint Patterns
### JSONB Fields with Timestamps
**Problem:** Storing datetime objects directly in JSONB fields causes serialization errors.
**Solution:** Convert to ISO string before storing:
```python
decision = {
"node_id": node_id,
"answer": answer,
"timestamp": datetime.now(timezone.utc).isoformat() # String, not datetime
}
```
---
### Soft Delete Pattern
**Pattern:** Use `is_active` boolean instead of actually deleting records.
**Note:** Our schema uses `is_active`, not `is_deleted` (documentation was corrected on Jan 28, 2026).
---
## Testing
### Test Database Setup
**Requirement:** Tests need a separate database.
**One-time setup:**
```powershell
docker exec -it patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_test;"
```
**Run tests:**
```powershell
cd backend
pytest
```
---
## Common Mistakes to Avoid
| Mistake | Correct Approach |
|---------|------------------|
| Using `datetime.utcnow()` | Use `datetime.now(timezone.utc)` |
| Running git from subdirectory | Always `cd` to project root first |
| Forgetting to activate venv | Check for `(venv)` prefix |
| Editing files in OneDrive folder | Use `C:\Dev\Projects\` |
| Using `psql` directly on Windows | Use `docker exec` instead |
| Storing datetime in JSON | Convert to `.isoformat()` string |
---
## Seed Scripts
### httpx Not Installed
**Problem:** Running `python -m scripts.seed_trees` fails with `ModuleNotFoundError: No module named 'httpx'`
**Cause:** The seed script uses `httpx` for async HTTP requests, which isn't in the base requirements.
**Solution:** Install httpx before running seed scripts:
```powershell
pip install httpx
```
---
### Email Validation Rejects .local Domains
**Problem:** Seed script fails with email validation error when using `.local` domain for seed user email.
**Error:** `value is not a valid email address: The part after the @-sign is a special-use or reserved name that cannot be used with email.`
**Cause:** The `email-validator` library (used by Pydantic) correctly rejects `.local` as it's a reserved TLD per RFC 6761.
**Solution:** Use a standard domain like `example.com` for seed/test users:
```python
# BAD
"email": "seed.admin@patherly.local"
# GOOD
"email": "seed.admin@example.com"
```
**Files affected:** `backend/scripts/seed_trees.py`, any test fixtures with email addresses
---
## Adding New Lessons
When you encounter and fix a bug, add it here with:
1. **Problem:** What error/symptom occurred
2. **Cause:** Why it happened (if known)
3. **Solution:** How to fix it
4. **Files affected:** Where to look (if applicable)