# CLAUDE.md - Patherly Project Context > **Purpose:** This file provides Claude Code with essential context for working on the Patherly project. > **Last Updated:** February 2, 2026 --- ## Project Overview **Patherly** is a troubleshooting decision tree application designed for MSP engineers. It guides engineers through proven troubleshooting paths, captures decisions and notes automatically, and generates professional ticket documentation. **Tagline:** "Take the path MOST traveled." **Primary User:** Michael Chihlas - Senior Systems Engineer at an MSP **Goal:** Michael uses this tool for 50% of his tickets within 3 months. --- ## Current State - **Phase:** Phase 2 - Tree Editor (In Progress) - **Backend:** Complete (18 API endpoints, 40+ integration tests, all passing) - **Frontend:** Core features complete, Tree Editor functional - **Database:** PostgreSQL with Docker (container name: `patherly_postgres`) ### What's Complete - User authentication (JWT, register, login, refresh, invite codes) - Trees CRUD with full-text search - Sessions tracking with decisions - Export API (Markdown, Text, HTML) - Tree Editor with form-based editing and visual preview - Dark/Light theme toggle - Markdown rendering in session player and node editor - 7 comprehensive seed decision trees - **Tree Organization System:** - Categories (global + team-specific, admin-managed) - Tags (author + admin managed, autocomplete) - User folders (personal tree collections) - Subfolder hierarchy (max 3 levels deep) - Right-click context menu for edit/delete/add subfolder - Cascade delete for subfolders - Team admin role with scoped permissions - Filter trees by category, tags, and folders ### What's In Progress - User preferences (export format default) ### Deployment - **Production:** Railway (app.patherly.com / api.patherly.com) - **PR Environments:** Enabled - auto-created for each pull request --- ## Tech Stack ### Backend - **Framework:** Python FastAPI - **Database:** PostgreSQL 16 (async via SQLAlchemy 2.0 + asyncpg) - **Migrations:** Alembic - **Auth:** JWT tokens (python-jose) + bcrypt passwords - **Validation:** Pydantic v2 ### Frontend - **Framework:** React 19 + Vite + TypeScript - **Styling:** Tailwind CSS v3 - **State:** Zustand (with immer + zundo for undo/redo) - **Routing:** React Router v7 - **API Client:** Axios with token interceptors - **Icons:** Lucide React --- ## Project Structure ``` patherly/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI entry point │ │ ├── api/ │ │ │ ├── endpoints/ │ │ │ │ ├── auth.py # Auth: register, login, refresh, logout │ │ │ │ ├── trees.py # Trees CRUD + search │ │ │ │ ├── sessions.py # Sessions + export │ │ │ │ └── invite.py # Invite code management │ │ │ ├── deps.py # Auth dependencies │ │ │ └── router.py │ │ ├── core/ │ │ │ ├── config.py # Settings (pydantic-settings) │ │ │ ├── database.py # Async SQLAlchemy │ │ │ ├── security.py # JWT + password hashing │ │ │ ├── logging_config.py # Structured logging │ │ │ └── middleware.py # Request logging │ │ ├── models/ # SQLAlchemy models │ │ │ ├── user.py # is_team_admin field added │ │ │ ├── team.py │ │ │ ├── tree.py # JSONB tree_structure + category_id, tags │ │ │ ├── session.py # JSONB path_taken, decisions │ │ │ ├── attachment.py │ │ │ ├── invite_code.py │ │ │ ├── category.py # TreeCategory model (NEW) │ │ │ ├── tag.py # TreeTag model (NEW) │ │ │ └── folder.py # UserFolder model (NEW) │ │ └── schemas/ # Pydantic schemas │ ├── alembic/ # Database migrations │ ├── scripts/ │ │ ├── seed_data.py │ │ └── seed_trees.py # 7 comprehensive trees │ ├── tests/ # pytest integration tests │ ├── docker-compose.yml # PostgreSQL container │ ├── requirements.txt │ └── .env # Environment variables │ ├── frontend/ │ ├── src/ │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── router.tsx │ │ ├── api/ # Axios API client │ │ │ ├── client.ts # Axios instance with interceptors │ │ │ ├── auth.ts │ │ │ ├── trees.ts │ │ │ └── sessions.ts │ │ ├── store/ │ │ │ ├── authStore.ts # Zustand auth state │ │ │ ├── themeStore.ts # Dark/light theme │ │ │ └── treeEditorStore.ts # Tree editor state (immer + zundo) │ │ ├── components/ │ │ │ ├── common/ # Modal, ErrorBoundary, ThemeToggle │ │ │ ├── layout/ # AppLayout, ProtectedRoute │ │ │ ├── tree-editor/ # Tree editor components │ │ │ ├── tree-preview/ # Visual tree preview │ │ │ └── ui/ # MarkdownContent │ │ ├── pages/ │ │ │ ├── LoginPage.tsx │ │ │ ├── RegisterPage.tsx │ │ │ ├── TreeLibraryPage.tsx │ │ │ ├── TreeNavigationPage.tsx # Core feature │ │ │ ├── TreeEditorPage.tsx │ │ │ ├── SessionHistoryPage.tsx │ │ │ └── SessionDetailPage.tsx │ │ ├── types/ # TypeScript interfaces │ │ └── lib/utils.ts # cn() utility for Tailwind │ ├── package.json │ ├── tailwind.config.js │ └── vite.config.ts │ ├── CLAUDE.md # This file ├── CURRENT-STATE.md # Quick status reference ├── LESSONS-LEARNED.md # Bugs and fixes (READ THIS!) ├── PROGRESS.md # Detailed progress log ├── 01-PROJECT-OVERVIEW.md # Vision and goals ├── 02-TECHNICAL-ARCHITECTURE.md # System design, API specs ├── 03-DEVELOPMENT-ROADMAP.md # Phases and timeline ├── 04-FEATURE-SPECIFICATIONS.md # Feature details ├── 05-QUESTIONS-AND-ACTION-ITEMS.md └── PHASE-2.5-PERSONAL-BRANCHING.md # Future feature spec ``` --- ## Development Commands ### Start Development Environment ```powershell # Terminal 1: Start PostgreSQL docker start patherly_postgres # Terminal 2: Backend cd C:\Dev\Projects\patherly\patherly\backend .\venv\Scripts\Activate uvicorn app.main:app --reload # Terminal 3: Frontend cd C:\Dev\Projects\patherly\patherly\frontend npm run dev ``` ### URLs - Frontend: http://localhost:5173 - Backend API: http://localhost:8000 - API Docs: http://localhost:8000/api/docs ### Run Tests ```powershell cd C:\Dev\Projects\patherly\patherly\backend .\venv\Scripts\Activate pytest ``` ### Run Seed Scripts ```powershell cd C:\Dev\Projects\patherly\patherly\backend .\venv\Scripts\Activate pip install httpx # Required for seed scripts python -m scripts.seed_trees ``` ### Database Operations ```powershell # Run migrations cd backend alembic upgrade head # Create new migration alembic revision --autogenerate -m "Description" # Access PostgreSQL (no local psql needed) docker exec -it patherly_postgres psql -U postgres -d patherly ``` --- ## Critical Lessons Learned **ALWAYS read [LESSONS-LEARNED.md](LESSONS-LEARNED.md) before making changes!** ### DateTime Handling (Critical) ```python # CORRECT - Always use timezone-aware datetimes from datetime import datetime, timezone from sqlalchemy import DateTime created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) # WRONG - Never use this datetime.utcnow() # Deprecated, returns naive datetime ``` ### React State: Don't Store Object Snapshots ```tsx // WRONG - Snapshot won't update when store changes const [editingNode, setEditingNode] = useState(null) // CORRECT - Store ID only, fetch current object each render const [editingNodeId, setEditingNodeId] = useState(null) const editingNode = editingNodeId ? findNode(editingNodeId) : null ``` ### Modal Draft State: Don't Overwrite Store-Managed Fields ```tsx // WRONG - Overwrites children with stale snapshot const handleSave = () => { updateNode(node.id, draft) // draft.children is stale! } // CORRECT - Exclude store-managed fields const handleSave = () => { const { children, ...draftWithoutChildren } = draft updateNode(node.id, draftWithoutChildren) } ``` ### Database Name - Database name is `patherly` (not `decision_tree`) - Update `.env` if you see the old name ### Virtual Environment - Always check for `(venv)` prefix before running pip - Don't use `--break-system-packages` when venv is active ### PostgreSQL NULL Casting for UUID Columns ```sql -- WRONG - PostgreSQL infers NULL as text type INSERT INTO tree_tags (name, slug, team_id) SELECT 'tag', 'slug', NULL as team_id -- Error: column is uuid but expression is text -- CORRECT - Explicitly cast NULL to uuid INSERT INTO tree_tags (name, slug, team_id) SELECT 'tag', 'slug', NULL::uuid as team_id -- Works! ``` Always use `NULL::uuid` when inserting NULL values into UUID columns in raw SQL. ### SQLAlchemy Async: Avoid Lazy Loading on New Objects ```python # WRONG - Triggers lazy load which fails in async context new_tree = Tree(...) db.add(new_tree) await db.flush() new_tree.tags.append(tag) # MissingGreenlet error! # CORRECT - Use direct SQL for junction tables from app.models.tag import tree_tag_assignments await db.execute( tree_tag_assignments.insert().values( tree_id=new_tree.id, tag_id=tag.id ) ) ``` Accessing relationships on newly created objects triggers lazy loading, which fails in async SQLAlchemy. Use direct SQL inserts for junction tables instead. ### React Router: Clear Dirty State Before Navigation ```tsx // WRONG - Navigation triggers before dirty flag is cleared const newTree = await treesApi.create(data) navigate(`/trees/${newTree.id}/edit`) // Blocker fires here! markSaved() // Too late // CORRECT - Clear dirty state first const newTree = await treesApi.create(data) markSaved() // Clear isDirty first navigate(`/trees/${newTree.id}/edit`) // Blocker won't fire ``` When using `useBlocker` for unsaved changes, always clear the dirty flag before programmatic navigation. --- ## API Endpoints Reference ### Authentication ``` POST /api/v1/auth/register - Register (requires invite code by default) POST /api/v1/auth/login - Login (form data) POST /api/v1/auth/login/json - Login (JSON body) POST /api/v1/auth/refresh - Refresh access token GET /api/v1/auth/me - Get current user POST /api/v1/auth/logout - Logout ``` ### Trees ``` GET /api/v1/trees - List trees (filters: category_id, tags, folder_id) POST /api/v1/trees - Create tree (supports tags, category_id) GET /api/v1/trees/categories - Get legacy string categories GET /api/v1/trees/search - Full-text search GET /api/v1/trees/{id} - Get tree (includes tags, category_info) PUT /api/v1/trees/{id} - Update tree DELETE /api/v1/trees/{id} - Soft delete ``` ### Categories (NEW) ``` GET /api/v1/categories - List categories (global + user's team) POST /api/v1/categories - Create category (team/global admin) GET /api/v1/categories/{id} - Get category PUT /api/v1/categories/{id} - Update category DELETE /api/v1/categories/{id} - Soft delete category ``` ### Tags (NEW) ``` GET /api/v1/tags - List tags (global + user's team) GET /api/v1/tags/search - Autocomplete search POST /api/v1/tags - Create tag GET /api/v1/tags/{id} - Get tag GET /api/v1/tags/trees/{id} - Get tree's tags POST /api/v1/tags/trees/{id} - Add tags to tree PUT /api/v1/tags/trees/{id} - Replace tree's tags DELETE /api/v1/tags/trees/{id}/{slug} - Remove tag from tree ``` ### Folders (NEW) ``` GET /api/v1/folders - List user's folders (includes parent_id) POST /api/v1/folders - Create folder (supports parent_id for subfolders) GET /api/v1/folders/{id} - Get folder PUT /api/v1/folders/{id} - Update folder (supports moving via parent_id) DELETE /api/v1/folders/{id} - Delete folder (cascades to subfolders) POST /api/v1/folders/reorder - Reorder folders POST /api/v1/folders/{id}/trees - Add tree to folder GET /api/v1/folders/{id}/trees - Get folder's tree IDs DELETE /api/v1/folders/{id}/trees/{tree_id} - Remove tree from folder ``` **Folder hierarchy constraints:** - Max nesting depth: 3 levels (root → child → grandchild) - Same folder name allowed under different parents - Moving folders validates cycle prevention ### Sessions ``` GET /api/v1/sessions - List user's sessions POST /api/v1/sessions - Start session GET /api/v1/sessions/{id} - Get session PUT /api/v1/sessions/{id} - Update session POST /api/v1/sessions/{id}/complete - Complete session POST /api/v1/sessions/{id}/export - Export (md/txt/html) ``` ### Invite Codes ``` GET /api/v1/invite-codes - List codes (admin) POST /api/v1/invite-codes - Create code (admin) GET /api/v1/invite-codes/validate/{code} - Validate code ``` --- ## Data Models ### Tree Structure (JSONB) ```typescript interface TreeStructure { id: string type: 'decision' | 'action' | 'solution' // Decision nodes question?: string help_text?: string options?: Array<{ id: string label: string next_node_id?: string }> // Action nodes title?: string description?: string commands?: string[] next_node_id?: string // Solution nodes title?: string description?: string steps?: string[] // All nodes can have children children?: TreeStructure[] } ``` ### Session Decisions (JSONB) ```typescript interface Decision { node_id: string question?: string answer: string notes?: string timestamp: string // ISO string, not datetime object } ``` --- ## Frontend Patterns ### State Management - **Auth:** `useAuthStore` - Zustand with localStorage persistence - **Theme:** `useThemeStore` - Dark/light/system preference - **Tree Editor:** `useTreeEditorStore` - Zustand + immer + zundo (undo/redo) ### Component Guidelines - Use `cn()` from `@/lib/utils` for Tailwind class merging - Use Lucide icons (no `title` prop - wrap in `` instead) - Modals: Use fixed header/footer with scrollable body - Forms: Show field-level validation errors ### API Client Pattern ```typescript import api from '@/api/client' // Token refresh handled automatically by interceptor const response = await api.get('/api/v1/trees') ``` --- ## Common Tasks ### Adding a New API Endpoint 1. Create route in `backend/app/api/endpoints/` 2. Add to router in `backend/app/api/router.py` 3. Create/update Pydantic schemas in `backend/app/schemas/` 4. Add tests in `backend/tests/` 5. Update API client in `frontend/src/api/` ### Adding a New Page 1. Create page component in `frontend/src/pages/` 2. Add route in `frontend/src/router.tsx` 3. Add navigation link in `AppLayout.tsx` if needed ### Modifying Database Schema 1. Update model in `backend/app/models/` 2. Create migration: `alembic revision --autogenerate -m "description"` 3. Review generated migration file 4. Apply: `alembic upgrade head` --- ## Coding Standards ### Python (Backend) - Use type hints everywhere - Use async/await for database operations - Use Pydantic for validation - Log with correlation IDs for tracing - Always use `DateTime(timezone=True)` for timestamps ### TypeScript (Frontend) - Enable strict mode (when ready) - Use TypeScript interfaces for all data structures - Prefer `const` over `let` - Use functional components with hooks - Extract reusable logic into custom hooks ### Git - Commit message format: `type: description` - Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore` - Always include `Co-Authored-By: Claude Opus 4.5 ` --- ## Future Roadmap ### Phase 2.5 (Planned) - Personal tree branching (add custom steps during sessions) - Step library with categories, tags, and ratings - Tree forking and sharing ### Phase 3 (Planned) - File attachments (screenshots, logs) - Offline mode (Service Workers + IndexedDB) - Client context system - Analytics dashboard ### Phase 4 (Planned) - API & integrations (ConnectWise, Kaseya) - PowerShell automation execution - Enterprise features (SSO, white-labeling) --- ## Troubleshooting ### Backend won't start 1. Check Docker: `docker ps` - is `patherly_postgres` running? 2. Check `.env` - is DATABASE_URL correct (`patherly` not `decision_tree`)? 3. Check venv: is `(venv)` prefix showing? ### Frontend compile errors 1. Check `frontend/src/lib/utils.ts` exists (provides `cn()` function) 2. Run `npm install` to ensure dependencies are installed ### Tests failing 1. Create test database: `docker exec -it patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_test;"` 2. Install dev deps: `pip install -r requirements-dev.txt` 3. Ensure pytest-asyncio version: `pip install pytest-asyncio==0.24.0` ### API 500 errors 1. Check server logs for datetime errors (timezone issue) 2. Ensure all datetimes use `datetime.now(timezone.utc)` --- ## Quick Reference | What | Where | |------|-------| | API Docs | http://localhost:8000/api/docs | | Current Status | [CURRENT-STATE.md](CURRENT-STATE.md) | | Bug Fixes | [LESSONS-LEARNED.md](LESSONS-LEARNED.md) | | Feature Specs | [04-FEATURE-SPECIFICATIONS.md](04-FEATURE-SPECIFICATIONS.md) | | Phase 2.5 Spec | [PHASE-2.5-PERSONAL-BRANCHING.md](PHASE-2.5-PERSONAL-BRANCHING.md) | --- ## Railway Deployment ### Production - **Frontend:** https://app.patherly.com - **Backend:** https://api.patherly.com - **Database:** Railway-managed PostgreSQL - Deploys automatically on push to `main` ### PR Environments Railway creates isolated preview environments for each pull request. **Workflow:** 1. Create feature branch: `git checkout -b feature/my-feature` 2. Make changes, commit, push to origin 3. Open Pull Request on GitHub 4. Railway auto-creates preview environment (backend + frontend + DB) 5. **Generate domains manually** in Railway dashboard: - Switch to the PR environment - Click on each service → Settings → Networking → Generate Domain 6. **Set `VITE_API_URL`** on frontend service to point to the PR backend URL 7. Redeploy frontend if needed 8. Test at preview URLs 9. Merge PR → auto-deploys to production 10. Railway cleans up PR environment after merge **Environment Variables:** - PR environments inherit from `production` base environment - `REQUIRE_INVITE_CODE=true` is inherited (create invite codes in PR DB if needed) - `DATABASE_URL` is auto-provided for isolated PR database **Notes:** - Each PR gets a fresh database - no existing users/trees - Migrations run automatically via `releaseCommand` - Domains must be generated manually for each PR service --- ## Contact **Primary User:** Michael Chihlas **Communication:** GitHub Issues / Direct chat