Tree Editor Features: - Zustand store with immer middleware and zundo for undo/redo - Form-based node editing (Decision, Action, Solution types) - Visual tree preview with solution connection indicators - NodePicker with type-grouped dropdown (Decisions/Actions/Solutions) - SharedLinksMap for detecting nodes with multiple sources - Modal component with scrollable body, fixed header/footer New Components: - TreeEditorLayout, TreeMetadataForm, NodeList, NodeEditorModal - NodeFormDecision, NodeFormAction, NodeFormResolution - DynamicArrayField, NodePicker - TreePreviewPanel, TreePreviewNode Documentation: - Updated README.md status to Phase 2 - Added Tree Editor details to CURRENT-STATE.md - Added modal/Zustand lessons to LESSONS-LEARNED.md - Updated file structure in CLAUDE-SETUP.md - Added Tree Editor progress to PROGRESS.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
438 lines
12 KiB
Markdown
438 lines
12 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 28, 2026
|
|
|
|
---
|
|
|
|
## 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 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 apoklisis_postgres psql -U postgres -c "SELECT * FROM users;"
|
|
|
|
# Interactive session
|
|
docker exec -it apoklisis_postgres psql -U postgres
|
|
|
|
# Create database
|
|
docker exec -it apoklisis_postgres psql -U postgres -c "CREATE DATABASE apoklisis_test;"
|
|
```
|
|
|
|
---
|
|
|
|
### Docker Container Not Running
|
|
**Problem:** Database connection errors.
|
|
|
|
**Solution:** Check and start the container:
|
|
```powershell
|
|
docker ps # See running containers
|
|
docker start apoklisis_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\Apoklisis
|
|
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 apoklisis_postgres psql -U postgres -c "CREATE DATABASE apoklisis_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 |
|
|
|
|
---
|
|
|
|
## 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)
|