# 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= 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 ``. 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 -->