Files
resolutionflow/LESSONS-LEARNED.md
Michael Chihlas 4cee013733 Implement Tree Editor with visual preview and documentation updates
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>
2026-01-28 03:00:00 -05:00

12 KiB

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:

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

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:

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:

// 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:

// 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)


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:

// 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:

<CheckCircle title="Has solution endpoint" /> // ❌ Error

Solution: Wrap the icon in a span with the title:

<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:

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:

// 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:

{
  "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:

module.exports = {
  darkMode: 'class', // or 'media' for system preference only
  // ...
}

Usage:

<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:

# 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:

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:

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:

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:

docker exec -it apoklisis_postgres psql -U postgres -c "CREATE DATABASE apoklisis_test;"

Run tests:

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)