Add CW security roles reference docs and PSA ticket management plan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
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_typecolumn 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
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)
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)
VITE_API_URL=http://localhost:8000
ConnectWise PSA Integration
All reference materials in docs/connectwise/. See 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— Pinapplication/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)
docs/connectwise/CONNECTWISE-API-REFERENCE.md— Auth patterns, endpoint map, field mappings.docs/connectwise/connectwise-psa-resolutionflow-reference.json— Extracted OpenAPI 3.0.1 spec (670 endpoints, 342 schemas).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) +clientIdheader on every request clientIdis server-side config (CW_CLIENT_IDinconfig.py) — identifies ResolutionFlow app, NOT per-tenant. Per-connection:company_id,public_key,private_key,server_url- All PSA code in
services/psa/—PSAProviderabstract base,ConnectWiseProviderimpl,PsaProviderRegistryfor 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.pyfor 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
# 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+ validteam_id - Backend deps:
get_current_active_user,require_engineer_or_admin(blocks viewers),require_admin(super admin only) - Never use
role == "admin"— useis_super_admininstead - 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 — 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+ 1pxborder-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) + 1pxborder-default, 5px radius. Focus:border-color: accent+box-shadow: 0 0 0 2px accent-dim - Text:
text-heading→text-primary→text-muted-foreground(#848b9b). NEVERtext-secondary— maps to a dark surface color. - Functional colors:
#34d399success,#fbbf24warning,#f87171danger,#67e8f9info — each has-dimat 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 fromtypes/index.ts, import withimport type { T } from '@/types' - Custom step flow:
CustomStepModal→PostStepActionModal→ContinuationModal. UsefindCustomStep()notfindNode()for custom step UUIDs. - Session sharing:
ShareSessionModal+SharedSessionPage. Utils inlib/sessionShare.ts. Share URLs:/shared/sessions/:token. - Routing helper: Use
getTreeNavigatePath()andgetTreeEditorPath()from@/lib/routingfor all tree/session navigation. - Account section:
AccountLayouthas NO sidebar nav. New account pages: route underaccountchildren inrouter.tsx+ link card inAccountSettingsPage. - Dashboard cockpit:
QuickStartPage—StartSessionInput+PendingEscalations,ActiveFlowPilotSessions,RecentFlowPilotSessions. Collapsible section forPerformanceCards,KnowledgeBaseCards,TeamSummary. - Sidebar: Amber "New Session" → Home → RESOLVE → KNOWLEDGE (Flows, Scripts) → INSIGHTS. Footer: Account, Pin/Unpin.
Common Tasks
- New endpoint: Create in
endpoints/→ add torouter.py→ schema inschemas/→ tests → frontend API client - New page: Create in
pages/→ route inrouter.tsx→ nav link inAppLayout.tsx - New public route: Add at top level in
router.tsx(alongside/login) — NOT insideProtectedRoute/AppLayout - Schema change: Update model →
alembic revision -m "desc"(no--rev-id) → review →alembic upgrade head - New frontend API module: Types in
types/→ export fromtypes/index.ts→ client inapi/→ export fromapi/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
- Update
CURRENT-STATE.md - Update
03-DEVELOPMENT-ROADMAP.md - Close related GitHub Issues:
gh issue close #N - Update
CLAUDE.mdif 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_URLwithhttps://prefix ALLOW_RAILWAY_ORIGINS=trueenables CORS for*.up.railway.app- Shared Variables auto-propagate to all PR envs — use for
ANTHROPIC_API_KEYetc. - 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 |
| Development Roadmap | 03-DEVELOPMENT-ROADMAP.md |
| GitHub Issues | gh issue list --state open |
| Design System | DESIGN-SYSTEM.md |
| Dev Environment | DEV-ENV.md — VPS setup, Docker, CORS, networking |
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 analyzein 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:
npx gitnexus analyze