- Extract useCustomStepFlow hook from TreeNavigationPage (1040 → 759 lines) - Create core/filters.py with shared tree/step visibility filters - Create services/export_service.py from session export logic - Add GitHub Actions CI/CD pipeline (pytest + lint + build) - Add GIN index migration for full-text search on trees - Update FastAPI 0.128.5, Pydantic 2.12.5, SQLAlchemy 2.0.46, +5 more - Fix regex → pattern deprecation in Query() params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
914 lines
36 KiB
Markdown
914 lines
36 KiB
Markdown
# CLAUDE.md - Patherly / ResolutionFlow Project Context
|
|
|
|
> **Purpose:** This file provides Claude Code with essential context for working on the Patherly project.
|
|
> **Last Updated:** February 5, 2026
|
|
|
|
---
|
|
|
|
## Project Overview
|
|
|
|
**Patherly** (user-facing brand: **ResolutionFlow**) is a **SaaS product for Managed Service Provider (MSP) professionals and companies**. It provides troubleshooting decision trees that guide engineers through proven troubleshooting paths, capture decisions and notes automatically, and generate professional ticket documentation.
|
|
|
|
**Tagline:** "Take the path MOST traveled."
|
|
|
|
**Target Market:** MSP (Managed Service Provider) companies — IT service providers that manage infrastructure, endpoints, and support for multiple client organizations.
|
|
|
|
**Primary User:** Michael Chihlas - Senior Systems Engineer at an MSP
|
|
|
|
**Goal:** Michael uses this tool for 50% of his tickets within 3 months.
|
|
|
|
**SaaS Context:** All features should be designed with multi-tenant MSP use in mind — teams represent MSP companies, trees are shared within teams, and the permission model supports tiered access (super_admin for platform, team_admin for MSP company, engineer for technicians, viewer for read-only access).
|
|
|
|
### Branding
|
|
|
|
The project was rebranded from "Patherly" to "ResolutionFlow" in the frontend (PR #26, commit `cfbd815`). The naming is split:
|
|
|
|
| Context | Name Used |
|
|
|---------|-----------|
|
|
| Repository / directory name | `patherly` |
|
|
| Backend (FastAPI, env vars, APP_NAME) | ResolutionFlow |
|
|
| Database / Docker container | `patherly` / `patherly_postgres` |
|
|
| Production URLs | `resolutionflow.com` / `api.resolutionflow.com` |
|
|
| **Frontend UI (header, login, register)** | **ResolutionFlow** |
|
|
| **Browser tab title** | **ResolutionFlow - Decision Tree Platform** |
|
|
|
|
**Brand details:**
|
|
|
|
- **Colors:** Purple gradient (`#818cf8` → `#a78bfa`)
|
|
- **Fonts:** Plus Jakarta Sans (headings), Inter (body), Outfit (labels) - loaded via Google Fonts
|
|
- **Logo:** Inline SVG in `AppLayout.tsx` and `LoginPage.tsx` (decision-tree icon with gradient)
|
|
- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
|
- **CSS utilities:** `text-gradient-brand`, `bg-gradient-brand`, `bg-gradient-brand-hover` (defined in `tailwind.config.js` and `index.css`)
|
|
- **Rebrand guide:** [REBRAND-IMPLEMENTATION-GUIDE.md](REBRAND-IMPLEMENTATION-GUIDE.md)
|
|
|
|
When adding new frontend pages or components, use "ResolutionFlow" for any user-visible branding. Use `font-heading` for heading elements and the `text-gradient-brand` utility for brand-colored text.
|
|
|
|
---
|
|
|
|
## Current State
|
|
|
|
- **Phase:** Phase 2.5 - Step Library Foundation (In Progress)
|
|
- **Backend:** Complete (25+ API endpoints, 60+ integration tests, all passing)
|
|
- **Frontend:** Core features complete, Tree Editor functional, Settings page added
|
|
- **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
|
|
- **User Preferences (Issue #3):**
|
|
- Settings page at `/settings`
|
|
- Default export format preference (persisted in localStorage)
|
|
- Theme toggle integrated in Settings
|
|
- **Step Categories (Issue #5):**
|
|
- Database table with 10 seeded global categories
|
|
- Full CRUD API at `/api/v1/step-categories`
|
|
- Team scoping support (global + team-specific)
|
|
- **Step Library Schema (Issue #6):**
|
|
- `step_library` table for reusable troubleshooting steps
|
|
- `step_ratings` table for user ratings/reviews
|
|
- `step_usage_log` table for tracking verified use
|
|
- Support for decision/action/solution step types
|
|
- Visibility levels: private, team, public
|
|
- **Step Library API (Issue #7):**
|
|
- Full CRUD at `/api/v1/steps`
|
|
- Full-text search endpoint
|
|
- Popular tags endpoint
|
|
- Rating/review system with verified use tracking
|
|
- **Frontend Rebrand (PR #26):**
|
|
- Renamed from "Patherly" to "ResolutionFlow" in all user-facing UI
|
|
- Purple gradient theme, custom fonts (Plus Jakarta Sans, Inter, Outfit)
|
|
- Custom SVG logo in header and auth pages
|
|
- Updated favicon and browser tab title
|
|
- **Token Refresh Fix:**
|
|
- Silent refresh with single-flight queue (prevents concurrent 401 race conditions)
|
|
- Backend `get_refresh_token_payload` dependency extracts refresh token from Authorization header
|
|
- Frontend Axios interceptor queues failed requests during refresh, retries after success
|
|
- Auth store synced after silent refresh via `setTokens` action
|
|
- **RBAC & Permissions:**
|
|
- `is_super_admin` boolean on User model (migration 010)
|
|
- Role hierarchy: super_admin > team_admin > engineer > viewer
|
|
- `role` field values: 'engineer' | 'viewer' (no more 'admin')
|
|
- Team Admin = `role='engineer'` + `is_team_admin=True` + valid `team_id`
|
|
- Backend centralized: `backend/app/core/permissions.py`
|
|
- Frontend hook: `frontend/src/hooks/usePermissions.ts`
|
|
- Viewers CAN: browse trees, start sessions, rate steps
|
|
- Viewers CANNOT: create/edit trees, steps, tags, categories
|
|
- **Security Hardening (Phase A):**
|
|
- Registration role field removed from `UserCreate` — hardcoded to `engineer`
|
|
- HTML export XSS fix — all user content escaped via `html.escape()`
|
|
- Secret key validator — rejects default key when `DEBUG=False`
|
|
- Role CHECK constraint on `users` table (migration 011)
|
|
- `test_admin` fixture fixed to properly grant `is_super_admin=True`
|
|
- **Security Hardening (Phase B):**
|
|
- B1: Tree access check on `start_session` via `can_access_tree` from permissions.py
|
|
- B2: All inline permission helpers replaced with centralized imports from `permissions.py`
|
|
- B3: `require_engineer_or_admin` checks `is_team_admin` before role check
|
|
- B4: `is_active` field on User model (migration 012), enforced in `get_current_active_user`
|
|
- B5: Admin user management endpoints at `/api/v1/admin/users/*` (6 endpoints)
|
|
- B6: Rate limiting on auth endpoints via slowapi (disabled when `DEBUG=True`)
|
|
- B7: Refresh token rotation — JTI-based revocation, meaningful logout (migration 013)
|
|
- Access token TTL reduced from 15 to 5 minutes
|
|
- All endpoints now use `get_current_active_user` (not `get_current_user`)
|
|
- **Permissions UX (Phase C):**
|
|
- Super admin bypass in tree list filter (`build_tree_access_filter` returns `sa_true()`)
|
|
- Audit log table (`audit_logs`) with JSONB details, integrated at admin + tree delete endpoints
|
|
- Soft delete for trees (`deleted_at` + `deleted_by` columns, migration 015)
|
|
- ProtectedRoute supports optional `requiredRole` prop for role-based route guards
|
|
- TreeEditorPage checks `canEditTree()` after fetch, before loading into editor
|
|
- Reusable `ConfirmDialog` component + tree delete UI on library page
|
|
- CustomStepModal hides "Type My Own" tab for users without `canCreateSteps`
|
|
- **Permissions Cleanup (Phase D):**
|
|
- Password complexity validation: requires uppercase, lowercase, and digit (min 10 chars)
|
|
- Soft delete cascade: cleans up folder/tag junction entries on tree delete
|
|
- Debug endpoint `/debug/cors` gated behind `if settings.DEBUG:`
|
|
- Tag search escapes SQL wildcards (`%`, `_`) before LIKE query
|
|
- **Session Scratchpad (Floating Overlay):**
|
|
- Fixed-position overlay panel (420px wide, 55vh tall) on right edge
|
|
- Floating button when collapsed, slide-in panel when expanded
|
|
- Ctrl+/ keyboard shortcut to toggle
|
|
- Auto-save with 1s debounce, markdown preview, localStorage persistence
|
|
- Main content adjusts width via padding transition when panel opens
|
|
- **Global Thin Scrollbar Styling:**
|
|
- 6px thin scrollbars site-wide (Firefox `scrollbar-width: thin` + WebKit pseudo-elements)
|
|
- Theme-aware colors using CSS variables (`--border`, `--muted-foreground`)
|
|
- **Admin Panel (Feb 2026):**
|
|
- Full admin panel at `/admin/*` with 8 pages (dashboard, users, invite codes, audit logs, plan limits, feature flags, settings, categories)
|
|
- Super admin access: requires `is_super_admin=true` on User model
|
|
- Admin API endpoints: `/api/v1/admin/*` (all require `require_admin` dependency)
|
|
- Utility script: `backend/make_superadmin_simple.py list|<email>` - promote users to super admin
|
|
- ActionMenu component: uses React Portal to avoid overflow clipping in tables
|
|
- **API Path Gotcha:** Frontend `apiClient` baseURL is `http://localhost:8000/api/v1` — all API calls should use relative paths WITHOUT `/api/v1/` prefix (e.g., `/admin/users` not `/api/v1/admin/users`)
|
|
|
|
### What's In Progress
|
|
|
|
- Custom step continuation flow refinements (Phase 2.5)
|
|
- Tree forking from sessions with custom steps
|
|
|
|
### 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 with ResolutionFlow brand theme (purple gradient)
|
|
- **Fonts:** Plus Jakarta Sans (headings), Inter (body), Outfit (labels) via Google Fonts
|
|
- **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
|
|
│ │ │ │ ├── admin.py # Admin user management (6 endpoints)
|
|
│ │ │ │ ├── trees.py # Trees CRUD + search
|
|
│ │ │ │ ├── sessions.py # Sessions + export
|
|
│ │ │ │ ├── invite.py # Invite code management
|
|
│ │ │ │ ├── categories.py # Tree categories (global + team)
|
|
│ │ │ │ ├── tags.py # Tree tags + autocomplete
|
|
│ │ │ │ ├── folders.py # User folders (hierarchy)
|
|
│ │ │ │ ├── steps.py # Step library CRUD + search
|
|
│ │ │ │ └── step_categories.py # Step categories
|
|
│ │ │ ├── deps.py # Auth dependencies
|
|
│ │ │ └── router.py
|
|
│ │ ├── core/
|
|
│ │ │ ├── config.py # Settings (pydantic-settings)
|
|
│ │ │ ├── database.py # Async SQLAlchemy
|
|
│ │ │ ├── audit.py # Centralized audit log helper (log_audit)
|
|
│ │ │ ├── permissions.py # Centralized RBAC (role checks, content guards)
|
|
│ │ │ ├── rate_limit.py # Shared slowapi limiter (disabled in DEBUG)
|
|
│ │ │ ├── security.py # JWT + password hashing + token rotation
|
|
│ │ │ ├── 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
|
|
│ │ │ ├── tag.py # TreeTag model
|
|
│ │ │ ├── folder.py # UserFolder model
|
|
│ │ │ ├── step_category.py # StepCategory model
|
|
│ │ │ ├── step_library.py # StepLibrary, StepRating, StepUsageLog
|
|
│ │ │ ├── refresh_token.py # RefreshToken model (JTI-based revocation)
|
|
│ │ │ └── audit_log.py # AuditLog model (JSONB details, indexed)
|
|
│ │ └── 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
|
|
│
|
|
├── brand-assets/ # Source brand SVGs and guides
|
|
│
|
|
├── frontend/
|
|
│ ├── public/
|
|
│ │ └── icons/ # Favicon and PWA icons
|
|
│ ├── src/
|
|
│ │ ├── main.tsx
|
|
│ │ ├── App.tsx
|
|
│ │ ├── router.tsx
|
|
│ │ ├── assets/brand/ # Brand logos (SVG)
|
|
│ │ ├── api/ # Axios API client
|
|
│ │ │ ├── client.ts # Axios instance with refresh queue interceptor
|
|
│ │ │ ├── auth.ts
|
|
│ │ │ ├── trees.ts
|
|
│ │ │ └── sessions.ts
|
|
│ │ ├── hooks/ # Custom React hooks (useKeyboardShortcuts, usePermissions)
|
|
│ │ ├── store/
|
|
│ │ │ ├── authStore.ts # Zustand auth state
|
|
│ │ │ ├── themeStore.ts # Dark/light theme
|
|
│ │ │ ├── treeEditorStore.ts # Tree editor state (immer + zundo)
|
|
│ │ │ └── userPreferencesStore.ts # User preferences (NEW)
|
|
│ │ ├── components/
|
|
│ │ │ ├── common/ # Modal, ErrorBoundary, ThemeToggle, ConfirmDialog
|
|
│ │ │ ├── layout/ # AppLayout, ProtectedRoute
|
|
│ │ │ ├── tree-editor/ # Tree editor components
|
|
│ │ │ ├── tree-preview/ # Visual tree preview
|
|
│ │ │ ├── step-library/ # Step library browser, forms, modals
|
|
│ │ │ ├── library/ # Tree library UI components
|
|
│ │ │ ├── session/ # Session modals, scratchpad floating overlay
|
|
│ │ │ └── ui/ # MarkdownContent
|
|
│ │ ├── pages/
|
|
│ │ │ ├── LoginPage.tsx
|
|
│ │ │ ├── RegisterPage.tsx
|
|
│ │ │ ├── TreeLibraryPage.tsx
|
|
│ │ │ ├── TreeNavigationPage.tsx # Core feature
|
|
│ │ │ ├── TreeEditorPage.tsx
|
|
│ │ │ ├── SessionHistoryPage.tsx
|
|
│ │ │ ├── SessionDetailPage.tsx
|
|
│ │ │ └── SettingsPage.tsx # User preferences (NEW)
|
|
│ │ ├── 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
|
|
├── REBRAND-IMPLEMENTATION-GUIDE.md # Patherly → ResolutionFlow rebrand guide
|
|
├── docs/plans/ # Design docs & implementation plans (YYYY-MM-DD-<topic>-design.md / -implementation.md)
|
|
├── 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
|
|
```
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
### Backend (.env)
|
|
|
|
**Required in `backend/.env`:**
|
|
```bash
|
|
# Application
|
|
APP_NAME=ResolutionFlow
|
|
DEBUG=true # Set false in production
|
|
|
|
# Database (matches docker-compose.yml)
|
|
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/patherly
|
|
DATABASE_URL_SYNC=postgresql://postgres:postgres@localhost:5432/patherly
|
|
|
|
# JWT Settings - CHANGE THESE IN PRODUCTION
|
|
SECRET_KEY=<generate with: openssl rand -hex 32>
|
|
ACCESS_TOKEN_EXPIRE_MINUTES=5 # Reduced from 15; refresh tokens handle session continuity
|
|
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
|
|
# Auth
|
|
REQUIRE_INVITE_CODE=true # Set false to allow open registration
|
|
```
|
|
|
|
**Railway-specific (production):**
|
|
```bash
|
|
ALLOW_RAILWAY_ORIGINS=true # Enables CORS for PR environments
|
|
```
|
|
|
|
### Frontend (.env.local - optional)
|
|
|
|
```bash
|
|
VITE_API_URL=http://localhost:8000 # Override API URL
|
|
```
|
|
|
|
---
|
|
|
|
## Development Commands
|
|
|
|
### Start Development Environment
|
|
|
|
```powershell
|
|
# Terminal 1: Start PostgreSQL
|
|
docker start patherly_postgres
|
|
|
|
# Terminal 2: Backend (from project root)
|
|
cd backend
|
|
# Windows:
|
|
.\venv\Scripts\Activate
|
|
# Linux/Mac:
|
|
source venv/bin/activate
|
|
uvicorn app.main:app --reload
|
|
|
|
# Terminal 3: Frontend (from project root)
|
|
cd frontend
|
|
npm run dev
|
|
```
|
|
|
|
### URLs
|
|
|
|
- Frontend: http://localhost:5173
|
|
- Backend API: http://localhost:8000
|
|
- API Docs: http://localhost:8000/api/docs
|
|
|
|
### Run Tests
|
|
|
|
```powershell
|
|
# From project root
|
|
cd backend
|
|
|
|
# First time only: create test database
|
|
docker exec -it patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_test;"
|
|
|
|
# Install test dependencies (if not already installed)
|
|
pip install -r requirements-dev.txt
|
|
|
|
# Run tests
|
|
pytest --override-ini="addopts="
|
|
```
|
|
|
|
### Frontend Operations
|
|
|
|
```powershell
|
|
# From project root
|
|
cd frontend
|
|
|
|
# Build for production
|
|
npm run build
|
|
|
|
# Preview production build
|
|
npm run preview
|
|
|
|
# Lint code
|
|
npm run lint
|
|
```
|
|
|
|
### Run Seed Scripts
|
|
|
|
```powershell
|
|
# From project root
|
|
cd backend
|
|
pip install httpx # Required for seed scripts
|
|
python -m scripts.seed_trees
|
|
```
|
|
|
|
### Database Operations
|
|
|
|
```powershell
|
|
# From project root
|
|
cd backend
|
|
|
|
# Run migrations
|
|
alembic upgrade head
|
|
|
|
# Create new migration
|
|
alembic revision --autogenerate -m "Description"
|
|
|
|
# Create migration without DB running (manual mode)
|
|
alembic revision -m "Description" # Edit migration file manually
|
|
# Migration runs when DB is available - safe to commit without testing locally
|
|
|
|
# Find current migration head (check down_revision in latest file)
|
|
# Chain mixes numeric IDs (001-008) and hex hashes (e.g., 4cdb5cba1aff)
|
|
# Always inspect the actual files — don't assume sequential ordering
|
|
|
|
# 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<TreeStructure | null>(null)
|
|
|
|
// CORRECT - Store ID only, fetch current object each render
|
|
const [editingNodeId, setEditingNodeId] = useState<string | null>(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.
|
|
|
|
### SQLAlchemy: Multiple FKs to Same Table Require `foreign_keys`
|
|
|
|
When a model has two FKs to the same table (e.g., Tree has `author_id` and `deleted_by` both pointing to `users`), specify `foreign_keys` on BOTH sides:
|
|
```python
|
|
# On Tree model
|
|
author = relationship("User", foreign_keys=[author_id], back_populates="trees")
|
|
# On User model
|
|
trees = relationship("Tree", foreign_keys="[Tree.author_id]", back_populates="author")
|
|
```
|
|
|
|
### 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.
|
|
|
|
### CORS: Include Both allow_origins AND allow_origin_regex
|
|
|
|
```python
|
|
# WRONG - Custom domains ignored when using regex
|
|
if settings.ALLOW_RAILWAY_ORIGINS:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origin_regex=r"https://.*\.up\.railway\.app", # Only matches Railway domains!
|
|
# ...
|
|
)
|
|
|
|
# CORRECT - Include both for custom domains + Railway PR environments
|
|
if settings.ALLOW_RAILWAY_ORIGINS:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.allowed_origins, # Custom domains like resolutionflow.com
|
|
allow_origin_regex=r"https://.*\.up\.railway\.app", # Railway PR domains
|
|
# ...
|
|
)
|
|
```
|
|
When using `allow_origin_regex` for wildcard patterns, also include `allow_origins` for explicit custom domains. The regex alone won't match custom domains like `resolutionflow.com`.
|
|
|
|
### RBAC Permission Checks
|
|
|
|
- Backend auth deps: `get_current_active_user` (any logged-in + active), `require_engineer_or_admin` (blocks viewers), `require_admin` (super admin only)
|
|
- Backend: `is_super_admin` replaces all `role == "admin"` checks. Never use `role == "admin"`.
|
|
- Frontend: use `usePermissions()` hook for all role/permission checks
|
|
- `TreeListItem` includes `team_id` for frontend permission checks (`author_id` and `team_id` are nullable)
|
|
|
|
### SQLAlchemy Test Fixtures: Circular FK Handling
|
|
|
|
The `users ↔ invite_codes` tables have circular foreign keys. `Base.metadata.drop_all()` fails with `CircularDependencyError`. The test fixture uses `DROP SCHEMA public CASCADE` instead (split into two `sa.text()` calls — asyncpg rejects multi-statement strings).
|
|
|
|
### Alembic Migrations: Test Data State Before Writing WHERE Clauses
|
|
|
|
Migration 010 had `WHERE role = 'admin'` but the only user already had `role = 'engineer'` (changed by earlier work), so the UPDATE matched zero rows. Always verify actual data values before writing conditional migrations, or use broader conditions.
|
|
|
|
### findNode Requires Tree Structure Parameter
|
|
|
|
```tsx
|
|
// WRONG - Always returns null (structure defaults to undefined)
|
|
const node = findNode(nodeId)
|
|
|
|
// CORRECT - Pass tree structure explicitly
|
|
const node = findNode(nodeId, tree?.tree_structure)
|
|
// For custom steps, also check:
|
|
const customStep = findCustomStep(nodeId)
|
|
```
|
|
|
|
### Frontend/Backend API Path Alignment
|
|
|
|
Always verify frontend API paths match backend route definitions. Example: backend serves `/steps/tags/popular` but frontend called `/steps/popular-tags`. Check `backend/app/api/router.py` and endpoint files for actual paths.
|
|
|
|
### Custom Step Flow in Tree Navigation
|
|
|
|
The custom step insertion flow in `TreeNavigationPage.tsx` chains through multiple modals:
|
|
|
|
1. `CustomStepModal` (create/browse) → 2. `PostStepActionModal` (save/use/both) → 3. `ContinuationModal` (pick descendant or build branch) → 4. Custom step view with "Continue to" button
|
|
|
|
Key state: `pendingStep`, `pendingContinuationNodeId`, `customBranchMode`, `branchOriginNodeId`
|
|
Custom steps are stored in session JSONB (`custom_steps` field) and referenced by UUID in `pathTaken`.
|
|
`findNode()` only searches tree structure -- use `findCustomStep()` for custom step UUIDs.
|
|
|
|
### Token Refresh: Match Frontend/Backend Contract
|
|
|
|
The refresh endpoint must accept tokens the same way the frontend sends them.
|
|
|
|
```python
|
|
# WRONG - Expects bare string, but frontend sends Authorization header
|
|
@router.post("/refresh")
|
|
async def refresh_token(refresh_token: str):
|
|
payload = decode_token(refresh_token)
|
|
|
|
# CORRECT - Use dependency that reads from Authorization header
|
|
@router.post("/refresh")
|
|
async def refresh_token(
|
|
payload: Annotated[dict, Depends(get_refresh_token_payload)],
|
|
):
|
|
```
|
|
|
|
The frontend Axios interceptor sends `Authorization: Bearer <refresh_token>`. The backend must extract it from the header, not expect it as a query/body parameter.
|
|
|
|
### CORS Errors Can Mask Server 500s
|
|
|
|
When the backend returns a 500 Internal Server Error, CORS headers are not added to the response. The browser reports this as a CORS error, hiding the real cause. Always check backend logs first when debugging CORS issues locally.
|
|
|
|
### Run Migrations Before Local Testing
|
|
|
|
After cloning or pulling new changes, always run `alembic upgrade head` before starting the backend. Missing migrations cause 500 errors (e.g., `column does not exist`) that manifest as CORS errors in the browser.
|
|
|
|
---
|
|
|
|
## API Endpoints Reference
|
|
|
|
**Full API documentation:** http://localhost:8000/api/docs (interactive OpenAPI/Swagger UI)
|
|
|
|
**Quick reference:**
|
|
- **Auth:** `/api/v1/auth/*` - register, login, refresh, logout, me
|
|
- **Trees:** `/api/v1/trees/*` - CRUD, search, categories (supports filters: category_id, tags, folder_id)
|
|
- **Sessions:** `/api/v1/sessions/*` - start, track, complete, export (markdown/text/html)
|
|
- **Tags:** `/api/v1/tags/*` - manage tree tags, autocomplete search
|
|
- **Folders:** `/api/v1/folders/*` - user folders with subfolder hierarchy (max 3 levels deep)
|
|
- **Categories:** `/api/v1/categories/*` - tree categories (global + team-specific)
|
|
- **Step Categories:** `/api/v1/step-categories/*` - step categories for library
|
|
- **Step Library:** `/api/v1/steps/*` - reusable steps, ratings, reviews, full-text search
|
|
- **Admin:** `/api/v1/admin/users/*` - user management (list, get, change role, toggle team admin, deactivate, activate)
|
|
- **Invite Codes:** `/api/v1/invite-codes/*` - admin management
|
|
|
|
**Key constraints:**
|
|
- Folder hierarchy: max 3 levels deep (root → child → grandchild)
|
|
- Step ratings: 1-5 stars with optional review text
|
|
- Categories and tags: support both global and team-specific scoping
|
|
|
|
For detailed parameters, request/response schemas, and examples, visit the API docs.
|
|
|
|
---
|
|
|
|
## Data Models
|
|
|
|
Key JSONB structures stored in PostgreSQL. See `frontend/src/types/` for full TypeScript interfaces.
|
|
|
|
- **Tree Structure:** Recursive node tree (`decision`/`action`/`solution` types with `children[]`). See `types/tree.ts`.
|
|
- **Session Decisions:** Array of `{node_id, question, answer, notes, timestamp}`. Timestamps are ISO strings, not datetime objects.
|
|
|
|
---
|
|
|
|
## Frontend Patterns
|
|
|
|
### State Management
|
|
|
|
- **Auth:** `useAuthStore` - Zustand with localStorage persistence (includes `setTokens` for silent refresh sync)
|
|
- **Theme:** `useThemeStore` - Dark/light/system preference
|
|
- **Tree Editor:** `useTreeEditorStore` - Zustand + immer + zundo (undo/redo)
|
|
- **User Preferences:** `useUserPreferencesStore` - Zustand with localStorage persistence (export format default)
|
|
|
|
### Component Guidelines
|
|
|
|
- Use `cn()` from `@/lib/utils` for Tailwind class merging
|
|
- Use Lucide icons (no `title` prop - wrap in `<span>` instead)
|
|
- Modals: Use fixed header/footer with scrollable body
|
|
- Modals: Import and render at end of parent component JSX
|
|
- Forms: Show field-level validation errors
|
|
- Conditional rendering: Add null checks when node might not exist (`currentNode && currentNode.type`)
|
|
|
|
### TypeScript Type Organization
|
|
|
|
- New type modules: Create in `types/` directory (e.g., `types/step.ts`)
|
|
- Export from `types/index.ts` with `export * from './modulename'`
|
|
- Import types using `import type { Type } from '@/types'` (type-only import)
|
|
|
|
### API Client Pattern
|
|
|
|
```typescript
|
|
import api from '@/api/client'
|
|
|
|
// Token refresh handled automatically by interceptor
|
|
// Concurrent 401s are queued — only one refresh request fires at a time
|
|
// On refresh failure, user is logged out and redirected to /login
|
|
const response = await api.get('/api/v1/trees')
|
|
```
|
|
|
|
### Floating Overlay Pattern (Scratchpad)
|
|
|
|
The scratchpad uses `position: fixed` with an `onOpenChange` callback so the parent page can adjust layout:
|
|
|
|
```tsx
|
|
// Child: ScratchpadSidebar.tsx
|
|
onOpenChange?: (isOpen: boolean) => void
|
|
// Fires when collapsed state changes, parent uses it to add/remove padding
|
|
|
|
// Parent: TreeNavigationPage.tsx
|
|
const [scratchpadOpen, setScratchpadOpen] = useState(...)
|
|
<div className={cn('...', scratchpadOpen && 'pr-[440px]')}>
|
|
<div className="mx-auto max-w-4xl"> {/* centers in available space */}
|
|
```
|
|
|
|
Position overlay at `right-2` (not `right-0`) so it sits inside the page scrollbar, and use full `rounded-lg` (not `rounded-l-lg`).
|
|
|
|
---
|
|
|
|
## Common Tasks
|
|
|
|
- **New endpoint:** Create in `endpoints/` → add to `router.py` → schema in `schemas/` → tests → frontend API client
|
|
- **New page:** Create in `pages/` → add route in `router.tsx` → nav link in `AppLayout.tsx`
|
|
- **Schema change:** Update model → `alembic revision --autogenerate -m "desc"` → review → `alembic upgrade head`
|
|
- **New frontend API module:** Types in `types/` → export from `types/index.ts` → client in `api/` → export from `api/index.ts`
|
|
|
|
---
|
|
|
|
## 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.6 <noreply@anthropic.com>`
|
|
- Always create a feature branch BEFORE committing new work (not retroactively after committing to main)
|
|
- PR workflow: `git checkout -b feat/feature-name` → commit → `git push -u origin feat/feature-name` → `gh pr create`
|
|
|
|
### Commit Strategy for Large Features
|
|
|
|
- Break implementation into logical phases (foundation → components → integration)
|
|
- Commit after each phase with `npm run build` validation
|
|
- Phase commits enable easier debugging and rollback
|
|
- Example: API clients (Phase 1) → UI components (Phase 2) → Page integration (Phase 3)
|
|
|
|
---
|
|
|
|
## Future Roadmap
|
|
|
|
### Phase 2.5 (In Progress)
|
|
|
|
- Custom step continuation flow refinements
|
|
- Tree forking from sessions with custom steps
|
|
|
|
### Phase 3 — File attachments, offline mode, client context, analytics
|
|
### Phase 4 — PSA integrations (ConnectWise, Kaseya), PowerShell automation, enterprise SSO
|
|
|
|
---
|
|
|
|
## 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`
|
|
4. If `unrecognized arguments: --cov` error: run with `--override-ini="addopts="` or install `pytest-cov`
|
|
|
|
### API 500 errors
|
|
|
|
1. Check server logs for datetime errors (timezone issue)
|
|
2. Ensure all datetimes use `datetime.now(timezone.utc)`
|
|
|
|
---
|
|
|
|
## Git Patterns
|
|
|
|
**Always gitignore:**
|
|
```
|
|
# Secrets and local config
|
|
backend/.env
|
|
.claude.local.md
|
|
.claude/settings.local.json
|
|
|
|
# Dependencies
|
|
backend/venv/
|
|
frontend/node_modules/
|
|
|
|
# Build artifacts
|
|
frontend/dist/
|
|
backend/**/__pycache__/
|
|
*.pyc
|
|
|
|
# Railway CLI (local tooling only)
|
|
/node_modules/
|
|
/package.json
|
|
/package-lock.json
|
|
|
|
# IDE
|
|
.vscode/
|
|
.idea/
|
|
*.swp
|
|
```
|
|
|
|
---
|
|
|
|
## 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) |
|
|
| Rebrand Guide | [REBRAND-IMPLEMENTATION-GUIDE.md](REBRAND-IMPLEMENTATION-GUIDE.md) |
|
|
|
|
---
|
|
|
|
## Railway Deployment
|
|
|
|
### Production
|
|
|
|
- **Frontend:** <https://resolutionflow.com>
|
|
- **Backend:** <https://api.resolutionflow.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
|
|
- **IMPORTANT:** Must include `https://` prefix (e.g., `https://patherly-patherly-pr-24.up.railway.app`)
|
|
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
|
|
- `ALLOW_RAILWAY_ORIGINS=true` (shared variable) - enables CORS for all `*.up.railway.app` origins
|
|
|
|
**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
|
|
|
|
**Debug Endpoints (only when `DEBUG=True`):**
|
|
- `/debug/cors` - Check CORS configuration (requires `DEBUG=True` in environment)
|
|
|
|
### Railway CLI (Local)
|
|
|
|
Railway CLI installed via npm in project root (gitignored). Services: `patherly` (backend), `hopeful-liberation` (frontend), `Postgres` (DB).
|
|
|
|
```powershell
|
|
# Common commands (run from project root)
|
|
./node_modules/.bin/railway service status --all
|
|
./node_modules/.bin/railway service logs --service patherly
|
|
./node_modules/.bin/railway variables --service patherly
|
|
```
|
|
|
|
---
|
|
|
|
## Contact
|
|
|
|
**Primary User:** Michael Chihlas
|
|
**Communication:** GitHub Issues / Direct chat
|