# CLAUDE.md - Patherly / ResolutionFlow Project Context

> **Last Updated:** April 16, 2026

---

## Project Overview

**Patherly** (user-facing brand: **ResolutionFlow**) is a **SaaS product for MSP professionals**. It provides troubleshooting decision trees that guide engineers through proven troubleshooting paths, capture decisions and notes, and generate professional ticket documentation.

**Target Market:** MSP companies — IT service providers managing infrastructure and support for multiple clients.

**SaaS Context:** Multi-tenant design — teams represent MSP companies, trees shared within teams, tiered access (super_admin, team_admin, engineer, viewer).

### Branding

| Context | Name Used |
|---------|-----------|
| Repository / directory / database | `patherly` (internal name) |
| Docker containers | `resolutionflow_postgres`, `resolutionflow_frontend`, `resolutionflow_backend` |
| Backend, frontend UI, production URLs | **ResolutionFlow** |

- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
- **Logo:** 30px gradient square (ember orange) + "ResolutionFlow" in Bricolage Grotesque 700
- **Layout:** Icon rail sidebar (72px default) with hover flyout panels. Pinnable to full 260px sidebar.
- **Terminology:** User-facing label is "Flows" (not "Trees"). Procedural flows are called "Projects" in the UI. Step Library is called "Solutions Library" in the UI. `tree_type` column values unchanged in DB.
- **Reference mockups:** `docs/mockups/` (HTML files, open in browser)

## Implementation Principles

- Prefer correct architecture over minimal diff
- If two approaches exist, implement the one that scales, not the one that's faster to write
- Flag any "simpler approach" tradeoffs for product owner review before proceeding

---

## Current State

- **Phase:** Go-to-Market Validation (Pre-PMF)
- **Backend:** Complete (55+ API endpoints, 100+ integration tests)
- **Frontend:** Core features complete, Tree Editor functional
- **Database:** PostgreSQL with Docker, 101 migrations
- **Detailed status:** [CURRENT-STATE.md](CURRENT-STATE.md)

### What's In Progress

- GTM validation: Shadow & Ship — founder dogfooding for 2 weeks, then 5 colleague pilot
- Solutions Library spec written (`docs/plans/2026-03-23-solutions-library-design.md`), implementation post-pilot
- Remaining open issues: #66 Templates + Import/Export, #60 Recurring Issue Detection, #58 Step Feedback Flag

---

## Tech Stack

### Backend
Python FastAPI, PostgreSQL 16 (async SQLAlchemy 2.0 + asyncpg), Alembic, JWT (python-jose) + bcrypt, Pydantic v2, APScheduler 3.x

### Frontend
React 19 + Vite + TypeScript, Tailwind CSS v4 (CSS-only config in `index.css`), Zustand (immer + zundo), React Router v7, Axios, Lucide React

---

## Key Project Structure

```
patherly/
├── backend/app/
│   ├── main.py                     # FastAPI entry point
│   ├── api/endpoints/              # Route handlers
│   ├── api/deps.py                 # Auth dependencies
│   ├── core/                       # config, database, permissions, security, audit, rate_limit
│   ├── models/                     # SQLAlchemy models
│   ├── schemas/                    # Pydantic schemas
│   └── services/psa/               # PSA provider abstraction (connectwise/, autotask/, halopsa/)
├── backend/alembic/                # Migrations (001-070 sequential, then hash IDs)
├── backend/tests/                  # pytest integration tests
├── frontend/src/
│   ├── api/                        # Axios client + endpoint modules
│   ├── components/                 # UI components
│   ├── hooks/                      # usePermissions, useSessionTimer, etc.
│   ├── pages/                      # Page components
│   ├── store/                      # Zustand stores
│   └── types/                      # TypeScript interfaces
└── docs/plans/                     # Design docs & implementation plans
```

---

## Environment Variables

### Backend (`backend/.env`)

```bash
APP_NAME=ResolutionFlow
DEBUG=true
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/patherly
DATABASE_URL_SYNC=postgresql://postgres:postgres@localhost:5432/patherly
SECRET_KEY=<openssl rand -hex 32>
ACCESS_TOKEN_EXPIRE_MINUTES=5
REFRESH_TOKEN_EXPIRE_DAYS=7
REQUIRE_INVITE_CODE=true
```

### Frontend (`frontend/.env.local` - optional)

```bash
VITE_API_URL=http://localhost:8000
```

---

## ConnectWise PSA Integration

All reference materials in `docs/connectwise/`. See [CONNECTWISE-API-REFERENCE.md](docs/connectwise/CONNECTWISE-API-REFERENCE.md) first.

### Best Practices Documentation

Read `docs/connectwise/best-practices/` BEFORE implementing any CW API integration code:

- `PSA-API-Requests.md` — HTTP methods, condition syntax, PATCH format. READ FIRST.
- `PSA-Callbacks.md` — Callback matrix, HMAC verification.
- `PSA-Pagination.md` — Forward-Only vs Navigable, Link headers.
- `PSA-Service-Tickets.md` — Ticket field mappings.
- `PSA-Versioning.md` — Pin `application/vnd.connectwise.com+json; version=2025.16`.
- `PSA-Cloud-URL-Formatting.md` — Dynamic base URL via `/login/companyinfo/{companyId}`.
- `Bundled-Requests.md` — Batch via `/system/bundles`.
- `PSA-Markdown.md` — Notes support markdown.
- `PSA-Company-Synchronization.md` — Filter companies by Status/Type.
- `PSA-Data-Protection.md` — Request minimal permissions (MY not ALL).

### Reference Files (read in this order)

1. `docs/connectwise/CONNECTWISE-API-REFERENCE.md` — Auth patterns, endpoint map, field mappings.
2. `docs/connectwise/connectwise-psa-resolutionflow-reference.json` — Extracted OpenAPI 3.0.1 spec (670 endpoints, 342 schemas).
3. `docs/connectwise/connectwise-psa-openapi-full.json` — Full spec (1838 endpoints). Only if you need something outside the subset.

### Key Implementation Rules

- Auth: API Key auth (Base64 of `companyId+publicKey:privateKey`) + `clientId` header on every request
- `clientId` is server-side config (`CW_CLIENT_ID` in `config.py`) — identifies ResolutionFlow app, NOT per-tenant. Per-connection: `company_id`, `public_key`, `private_key`, `server_url`
- All PSA code in `services/psa/` — `PSAProvider` abstract base, `ConnectWiseProvider` impl, `PsaProviderRegistry` for multi-PSA dispatch
- PSA endpoints in `api/endpoints/integrations.py` — connection CRUD, ticket ops, member mapping
- Credentials encrypted via `services/psa/encryption.py` (Fernet); stored per-team, never per-user
- In-memory TTL cache in `services/psa/cache.py` for board/status/priority lookups
- Integration flows: Session → Ticket Notes via `POST /service/tickets/{id}/notes`; Ticket Context → FlowPilot via ticket details/company/configs; Callbacks via `/system/callbacks`

---

## Development Commands

```bash
# PostgreSQL (run from VPS SSH — docker not available in code-server, see Lesson 103)
docker start resolutionflow_postgres

# Backend (from backend/)
source venv/bin/activate
uvicorn app.main:app --reload

# Frontend (from frontend/) — requires Node 20 (use nvm: nvm use 20)
npm run dev

# Tests (from backend/)
pytest --override-ini="addopts="

# TypeScript check (use in code-server — avoids EACCES on dist/, see Lesson 105)
npx tsc -b

# Frontend build — stricter than tsc, always use as final check before push
cd frontend && npm run build

# Migrations
cd backend && alembic upgrade head
alembic revision --autogenerate -m "Description"  # do NOT pass --rev-id; Alembic generates hash IDs

# Access PostgreSQL (VPS SSH)
docker exec -it resolutionflow_postgres psql -U postgres -d resolutionflow

# CI runs on Gitea (NOT GitHub Actions): https://gitea.resolutionflow.com/chihlasm/resolutionflow/actions
```

### URLs & Test Users

- Frontend: `http://localhost:5173` | Backend: `http://localhost:8000` | API Docs: `http://localhost:8000/api/docs`
- Test password: `TestPass123!` — users: `admin@`, `teamadmin@`, `engineer@`, `pro@` (all `@resolutionflow.example.com`)

---

## Critical Lessons Learned

> Lessons 1-70 archived to `docs/LESSONS-ARCHIVE.md` — fixes are baked into the codebase.

**71. Enhancement/branch_addition proposals cannot be directly approved:** Backend returns 400 — requires `modified_flow_data` via "Edit & Publish". Only `new_flow` proposals support direct approve.

**72. `ai_sessions.status` column is `VARCHAR(30)`:** Must fit `requesting_escalation` (23 chars). Verify length when adding new status values.

**73. `get_db` rolls back on exception:** Prevents `InFailedSQLTransaction` cascade. Never remove the `await session.rollback()` in the dependency.

**74. FlowPilot action bar height chain:** `ViewTransitionOutlet` wrapper needs `flex flex-col`. If action bar disappears, walk `getBoundingClientRect()` from `app-shell` down.

**75. Dashboard prefill auto-submits:** `StartSessionInput` passes `{ state: { prefill } }`. Both `FlowPilotSessionPage` and `AssistantChatPage` auto-submit via `useEffect` + `prefillHandledRef` guard.

**76. Active session navigation guard:** `FlowPilotSessionPage` uses `useBlocker` to intercept navigation. "Pause & Leave" auto-pauses before proceeding.

**77. Prefer manual Alembic migrations for targeted changes:** `--autogenerate` picks up all table drift. For single-column fixes, use `alembic revision -m "desc"` and write `op.alter_column()` manually.

**78. Landing page subtitle is "AI-Powered Troubleshooting for MSPs":** Appears on login, register, and `<title>`. Not "Decision Tree Platform".

**79. Custom modals must be mobile-responsive:** Use `items-end sm:items-center` + `max-w-full sm:max-w-lg`. See `Modal.tsx` and `PrepareSessionModal.tsx`.

**80. TopBar search collapses to icon on mobile:** Full bar (`hidden sm:block`) + icon fallback (`sm:hidden`). Both open `CommandPalette`.

**81. Never use `transition: all` in landing.css:** Specify exact properties. `transition: all` animates layout and causes jank.

**82. `bun` requires PATH setup:** `export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"`. Chromium deps: `libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2`.

**84. AI session `abandoned` status is fully wired:** `POST /ai-sessions/{id}/abandon` with optional `reason`. Frontend: `aiSessionsApi.abandonSession()` → `useFlowPilotSession().abandonSession()`.

**85. Date range filter end dates must use end-of-day:** Set `toDate.setHours(23, 59, 59, 999)`. For string inputs append `T23:59:59.999Z`. See `SessionHistoryPage.tsx`.

**86. Script Builder:** `/script-builder` — `ScriptBuilderSession` model, `script_builder_service.py`, endpoints at `/scripts/builder/`. FlowPilot handoff via `action_type: "open_script_builder"` + sessionStorage context.

**87. FlowPilot must ask GUI vs script preference:** Ask BEFORE suggesting either approach. See `FLOWPILOT_SYSTEM_PROMPT` in `flowpilot_engine.py`.

**88. Charcoal palette:** Sidebar `#0e1016`, page `#16181f`, cards `#1e2028`, borders `#2a2e3a`. All via CSS variables in `index.css` `@theme`. Accent is electric blue (#60a5fa).

**92. `tsc -b` in Dockerfile enforces `noUnusedLocals`/`noUnusedParameters` as hard errors.** After refactors, trace every import and destructured prop. Check IDE yellow squiggles before pushing.

**93. FlowPilot actions live in the page header, not a bottom bar:** Resolve/Escalate/Share Update in header. Desktop: inline + `⋯` overflow (Pause/Close). Mobile: single `⋯`. Bottom = message input only.

**94. Frontend chat uses `unified_chat_service`, not `assistant_chat_service`:** `AssistantChatPage` → `/ai-sessions/{id}/chat` → `unified_chat_service.py`. Never wire chat into `assistant_chat.py`.

**95. Image upload → AI vision:** `uploadsApi.upload()` → `upload_ids` in message → backend fetches S3 → `storage_service.resize_image_for_vision()` (Pillow, 1568px, PNG→JPEG) → base64 → Claude multimodal. Max 3 images/message. Images NOT stored in history.

**96. `bg-accent` is electric blue — never use for code/kbd.** Use `bg-code` for code blocks, `bg-white/[0.12]` for inline code/badges, `bg-white/[0.08]` for kbd.

**97. Railway S3 provisioned:** Bucket `resolutionflow-uploads`. Variables: `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY`, `STORAGE_BUCKET_NAME`, `STORAGE_REGION`. boto3 in `storage_service.py`.

**98. `lazyWithRetry` for lazy routes:** Use instead of `React.lazy` — auto-reloads on chunk failures with 10s sessionStorage debounce.

**99. `text-secondary` renders invisible on dark backgrounds:** Maps to `--color-secondary` (dark surface). Use `text-muted-foreground` (`#848b9b`) for readable secondary text. Never use `text-muted` for body text.

**100. Hover pop-out card pattern:** `pointer-events-none` on scrim (`z-40`), `z-50` expanded card with own `onClick`, dismiss via `onMouseLeave`. Never put handlers on scrim.

**101. AI marker format compliance:** `[QUESTIONS]`, `[ACTIONS]`, `[FORK]` parsed by `unified_chat_service.py`. History stores `display_content` (stripped). Each user message gets `[SYSTEM: ...]` reminder appended in `_call_anthropic_cached()`.

**102. TaskLane activation must happen in ALL chat response paths:** Three paths in `AssistantChatPage.tsx` — `handleSend`, `sendPrefill`, `handleResumeNew`. All must check `response.actions`/`response.questions` and call `setShowTaskLane(true)`.

**103. Docker not available in code-server:** Use VPS SSH: `docker exec resolutionflow_postgres psql -U postgres -d resolutionflow -t -c "SQL"`. Python also not available in container.

**104. `landing.css` uses `--lp-*` variables:** Never use `var(--color-*)` tokens in `landing.css`. Extend the `--lp-*` palette for new landing page colors.

**105. `npm run build` fails with `EACCES` on `dist/` in code-server:** Use `npx tsc -b` to verify TypeScript without writing to `dist/`.

**106. Guard async "select item → load data → apply state" flows:** Use `currentSelectionRef = useRef(id)` — update on every switch, bail after each `await` if ref no longer matches. See `AssistantChatPage.tsx` `currentChatRef`.

**107. Startup routines use `_admin_session_factory()`:** RLS is enabled; `get_db()` at startup has no `app.current_account_id`, so queries return 0 rows. Affects lifespan, `ensure_service_account`, seed scripts.

**108. Tables with no `account_id` (never add to RLS migrations):** `script_categories`, `platform_steps`, `template_trees`, `plan_feature_defaults`, `accounts`. Scan at class level, not file level — one `.py` file can have multiple classes with different columns.

**109. `tree_shares.account_id` must equal `tree.account_id`:** Use tree owner's tenant, not the actor's. Cross-tenant admin shares become invisible after RLS enforcement.

**110. Backfill `account_id` migrations require service-code audit:** Grep all `ModelClass(` sites, verify `account_id=` is passed. SQLAlchemy accepts `None` silently; RLS WITH CHECK surfaces it at runtime as `InsufficientPrivilegeError`.

**111. Global Axios interceptor fires before component `.catch()`:** Fix optional-data endpoints at the source — return `[]`/`{}` on provider failure instead of raising 502. See `list_boards` in `integrations.py`.

## RBAC & Permissions

- **Role hierarchy:** super_admin > team_admin > engineer > viewer
- **Team Admin:** `role='engineer'` + `is_team_admin=True` + valid `team_id`
- **Backend deps:** `get_current_active_user`, `require_engineer_or_admin` (blocks viewers), `require_admin` (super admin only)
- **Never use** `role == "admin"` — use `is_super_admin` instead
- **Frontend:** `usePermissions()` hook for all permission checks
- **Centralized:** `backend/app/core/permissions.py`, `frontend/src/hooks/usePermissions.ts`

---

## Design System

**Source of truth:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — always read before visual/UI decisions.

- **Theme:** Flat, high-contrast dark (Sentry/PostHog-inspired). No glass morphism, no backdrop blur, no gradients on surfaces. Fonts: IBM Plex Sans (body), Bricolage Grotesque (headings), JetBrains Mono (code).
- **Backgrounds:** `bg-page` (`#16181f`), `bg-sidebar` (`#0e1016`), `bg-card` (`#1e2028`), `bg-elevated` (`#2a2d38`)
- **Cards:** `bg-card` + 1px `border-default` (`#2a2e3a`), 8px radius. Hover: `border-hover` (`#3d4252`)
- **Buttons:** Primary: solid `accent` (#60a5fa / #2563eb), white text, 5px radius. Ghost: transparent + 1px border.
- **Inputs:** `bg-input` (`#252830`) + 1px `border-default`, 5px radius. Focus: `border-color: accent` + `box-shadow: 0 0 0 2px accent-dim`
- **Text:** `text-heading` → `text-primary` → `text-muted-foreground` (`#848b9b`). **NEVER `text-secondary`** — maps to a dark surface color.
- **Functional colors:** `#34d399` success, `#fbbf24` warning, `#f87171` danger, `#67e8f9` info — each has `-dim` at 10% opacity
- **Deprecated:** No `glass-card`, `backdrop-filter: blur()`, ambient orbs, ember orange (`#f97316`), or cyan as accent

---

## Frontend Patterns

- **Component guidelines:** Use `cn()` from `@/lib/utils`, Lucide icons (wrap in `<span>` for title), modals with fixed header/footer
- **Type organization:** Create in `types/`, export from `types/index.ts`, import with `import type { T } from '@/types'`
- **Custom step flow:** `CustomStepModal` → `PostStepActionModal` → `ContinuationModal`. Use `findCustomStep()` not `findNode()` for custom step UUIDs.
- **Session sharing:** `ShareSessionModal` + `SharedSessionPage`. Utils in `lib/sessionShare.ts`. Share URLs: `/shared/sessions/:token`.
- **Routing helper:** Use `getTreeNavigatePath()` and `getTreeEditorPath()` from `@/lib/routing` for all tree/session navigation.
- **Account section:** `AccountLayout` has NO sidebar nav. New account pages: route under `account` children in `router.tsx` + link card in `AccountSettingsPage`.
- **Dashboard cockpit:** `QuickStartPage` — `StartSessionInput` + `PendingEscalations`, `ActiveFlowPilotSessions`, `RecentFlowPilotSessions`. Collapsible section for `PerformanceCards`, `KnowledgeBaseCards`, `TeamSummary`.
- **Sidebar:** Amber "New Session" → Home → RESOLVE → KNOWLEDGE (Flows, Scripts) → INSIGHTS. Footer: Account, Pin/Unpin.

---

## Common Tasks

- **New endpoint:** Create in `endpoints/` → add to `router.py` → schema in `schemas/` → tests → frontend API client
- **New page:** Create in `pages/` → route in `router.tsx` → nav link in `AppLayout.tsx`
- **New public route:** Add at top level in `router.tsx` (alongside `/login`) — NOT inside `ProtectedRoute`/`AppLayout`
- **Schema change:** Update model → `alembic revision -m "desc"` (no `--rev-id`) → 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
Type hints everywhere, async/await for DB, Pydantic validation, `DateTime(timezone=True)` always.

### TypeScript
Interfaces for all data, `const` over `let`, functional components + hooks.

### Git
- Format: `type: description` (feat, fix, refactor, docs, test, chore)
- Always include `Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>`
- Create feature branch BEFORE committing: `git checkout -b feat/feature-name`
- **Remote is Gitea:** Push to `gitea.resolutionflow.com/chihlasm/resolutionflow`. Mirrors to GitHub via `.gitea/workflows/mirror-to-github.yml` — never push directly to GitHub.

### After Completing Work
1. Update `CURRENT-STATE.md`
2. Update `03-DEVELOPMENT-ROADMAP.md`
3. Close related GitHub Issues: `gh issue close #N`
4. Update `CLAUDE.md` if new patterns or lessons emerged

---

## gstack (Browser & Workflow Skills)

**Web browsing:** Always use `/browse`. Never use `mcp__claude-in-chrome__*` tools.

**Skills:** `/office-hours` · `/plan-ceo-review` · `/plan-eng-review` · `/plan-design-review` · `/design-consultation` · `/review` (PR review) · `/ship` · `/browse` (headless QA) · `/qa` (QA + fix) · `/qa-only` · `/design-review` (visual QA) · `/setup-browser-cookies` · `/retro` · `/investigate` · `/document-release` · `/codex` · `/careful` · `/freeze` · `/unfreeze` · `/guard` · `/gstack-upgrade`

---

## Deployment (Railway)

- **Production:** `resolutionflow.com` (frontend), `api.resolutionflow.com` (backend)
- Deploy pipeline: push to Gitea → mirrors to GitHub → Railway watches `main`
- PR envs: need manual domain generation + `VITE_API_URL` with `https://` prefix
- `ALLOW_RAILWAY_ORIGINS=true` enables CORS for `*.up.railway.app`
- Shared Variables auto-propagate to all PR envs — use for `ANTHROPIC_API_KEY` etc.
- Super admin: `backend/make_superadmin_simple.py list|<email>`

---

## Quick Reference

| What | Where |
|------|-------|
| API Docs | http://localhost:8000/api/docs |
| Detailed Status | [CURRENT-STATE.md](CURRENT-STATE.md) |
| Development Roadmap | [03-DEVELOPMENT-ROADMAP.md](03-DEVELOPMENT-ROADMAP.md) |
| GitHub Issues | `gh issue list --state open` |
| Design System | [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) |
| Dev Environment | [DEV-ENV.md](DEV-ENV.md) — VPS setup, Docker, CORS, networking |


<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **resolutionflow**. Use it selectively — for routine additive work (new endpoints, new components, isolated fixes) just read the files directly. GitNexus earns its cost when you're about to touch something genuinely central with many callers.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## When to Use It

**Use GitNexus when:**
- Touching a core shared symbol with many callers — `flowpilot_engine`, `unified_chat_service`, auth middleware, `get_db`, shared hooks
- Renaming anything used across multiple files
- Tracing an unfamiliar bug through a call chain you haven't read
- Assessing whether a refactor is safe before starting

**Skip GitNexus when:**
- Adding a new endpoint, component, or isolated feature
- Fixing a bug in a self-contained file
- Making changes you can already see the full scope of by reading the file

## Useful Tools

| Tool | When to use | Command |
|------|-------------|---------|
| `query` | Find code by concept when you don't know where to look | `gitnexus_query({query: "auth validation"})` |
| `context` | See all callers/callees of a symbol before touching it | `gitnexus_context({name: "symbolName"})` |
| `impact` | Blast radius check before editing a shared symbol | `gitnexus_impact({target: "X", direction: "upstream"})` |
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |

## Keeping the Index Fresh

A PostToolUse hook re-indexes automatically after `git commit`. To manually refresh:

```bash
npx gitnexus analyze
```

<!-- gitnexus:end -->
