3 Commits

Author SHA1 Message Date
19cfd71995 chore(flowpilot-migration): remove migration handoff note after verification
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Gate 1 complete on Proxmox dev host (docker-01):
- Alembic at f07010f17b01 (single head); downgrade/upgrade roundtrip clean.
- Phase 0 prompt-cache verified: direct provider probe shows
  cache_create=5398 → cache_read=5398 across two calls; chat path emitted
  two anthropic.cache events 55s apart on a real FlowPilot session.
- Frontend npm run build clean (57.63s, no TS errors, no stale
  FlowPilotSessionPage imports).
- /assistant/:id → /pilot/:id redirect fires correctly and session detail
  loads (GET /api/v1/ai-sessions/<id> 200); a blank-until-click UX polish
  will be tracked separately.
- Dashboard session-tile dispatcher routes to /pilot/:id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:21:08 -04:00
3b55697c77 dev-env(proxmox): switch compose to direct-port exposure; document homelab topology
- docker-compose.dev.yml: drop Traefik/dev.resolutionflow.com labels, expose
  backend:8000 and frontend:5173 directly; swap relative bind mounts for
  ${REPO_ROOT}/... so compose works when driven from inside a code-server
  container with the host Docker socket mounted; default POSTGRES_PORT to
  5433 host-side; add explicit uvicorn/npm run dev commands; add
  ENABLE_MCP_MICROSOFT_LEARN and docker-01/Tailscale CORS origins.
- frontend/vite.config.ts: replace dev.resolutionflow.com with
  allowedHosts=['docker-01', '.ts.net', 'localhost'] for direct-port access
  over the private network.
- DEV-ENV.md: add Section 11 reference topology for the homelab Proxmox +
  code-server Option B setup, plus troubleshooting entries for the
  REPO_ROOT-empty-mount trap and the Vite allowedHosts rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:18:31 -04:00
851966966d docs(claude-md): compact CLAUDE.md for 2026-04-19 baseline
Trim from 570 → 264 lines. Archived lessons and fixes-in-code remain in
docs/LESSONS-ARCHIVE.md; CLAUDE.md now only carries what a fresh session
can't derive from the repo state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:18:15 -04:00
5 changed files with 369 additions and 651 deletions

704
CLAUDE.md
View File

@@ -1,570 +1,264 @@
# CLAUDE.md - Patherly / ResolutionFlow Project Context
# CLAUDE.md ResolutionFlow
> **Last Updated:** April 6, 2026
> SaaS troubleshooting platform for MSPs. Last reviewed 2026-04-19.
**Naming:** Canonical product name is **ResolutionFlow**. `patherly` is the legacy internal name — still present in DB name (`patherly` on Railway, `resolutionflow` locally), some Railway service names, and historical paths. Treat as aliases, not canonical. Docker containers are `resolutionflow_*`.
**User terminology:** "Flows" (not Trees), "Projects" (not Procedures), "Solutions Library" (not Step Library). Maintenance flows hidden from pilot UI (backend retains them). DB column `tree_type` values unchanged.
**SaaS shape:** Multi-tenant by account. Roles: `super_admin` > `team_admin` > `engineer` > `viewer`. Team admin = `role='engineer'` + `is_team_admin=True` + valid `team_id`. Never `role=='admin'` — use `is_super_admin`. Backend deps in `app/api/deps.py`: `get_current_active_user`, `require_engineer_or_admin`, `require_admin`. Frontend: `usePermissions()` hook. Central logic in `backend/app/core/permissions.py` + `frontend/src/hooks/usePermissions.ts`.
**Status:** Go-to-Market Validation (pre-PMF). Backend feature-complete (55+ endpoints, 100+ tests). Phase 0.5 FlowPilot telemetry baseline accruing. See `CURRENT-STATE.md` for live status, `03-DEVELOPMENT-ROADMAP.md` for phases.
**Principle:** Prefer correct architecture over minimal diff. Flag "simpler approach" tradeoffs for review before taking them.
---
## Project Overview
## Tech stack
**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** |
- **Design system:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — THE source of truth for all design decisions
- **Design aesthetic:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no gradients on surfaces, no ambient effects. Light mode planned.
- **Accent color:** Electric blue (#60a5fa dark / #2563eb light). Used sparingly — ≤5% of the UI. Warning is amber (#fbbf24), info is cyan (#67e8f9).
- **Fonts:** IBM Plex Sans (`font-sans`, body), Bricolage Grotesque (`font-heading`, headings), JetBrains Mono (`font-mono`, code) — loaded via Google Fonts
- **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. See [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md)
- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
- **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. Maintenance flows are hidden from UI for pilot (backend still supports them). `tree_type` column values unchanged in DB.
- **Reference mockups:** `docs/mockups/` (HTML files, open in browser)
**Component styling:** See Design System section below and [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md). All colors via CSS variables. Use "Flows" not "Trees" in user-facing text; use "Projects" not "Procedures" for procedural flows.
## 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
- **Backend:** Python 3.11 + FastAPI, SQLAlchemy 2.0 async (asyncpg), Alembic, Pydantic v2, JWT (python-jose + bcrypt, JTI refresh rotation), APScheduler (in-process with FastAPI lifespan).
- **Frontend:** React 19 + Vite + TypeScript, Tailwind v4 (CSS-only config in `index.css`), Zustand (immer + zundo), React Router v7, Axios (token-refresh interceptor), Lucide.
- **DB:** PostgreSQL 16 (RLS enabled Phase 4, pgvector).
---
## 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
- **Framework:** Python FastAPI
- **Database:** PostgreSQL 16 (async via SQLAlchemy 2.0 + asyncpg)
- **Migrations:** Alembic
- **Auth:** JWT (python-jose) + bcrypt, refresh token rotation (JTI-based)
- **Validation:** Pydantic v2
- **Scheduling:** APScheduler 3.x (async, in-process with FastAPI lifespan) + croniter + pytz
### Frontend
- **Framework:** React 19 + Vite + TypeScript
- **Styling:** Tailwind CSS v4 (`@tailwindcss/vite` plugin, CSS-only config in `index.css`) — flat dark theme with ember orange accent (see [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md))
- **State:** Zustand (with immer + zundo for undo/redo)
- **Routing:** React Router v7
- **API Client:** Axios with token refresh interceptor
- **Icons:** Lucide React
---
## Key Project Structure
## Project structure
```
patherly/
resolutionflow/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI entry point
│ │ ├── api/endpoints/ # Route handlers (auth, trees, sessions, admin, steps, survey, copilot, assistant_chat, integrations)
│ │ │ ├── flow_proposals.py # Knowledge Flywheel review queue CRUD
│ │ │ └── flowpilot_analytics.py # FlowPilot dashboard metrics
│ │ ├── api/deps.py # Auth dependencies (includes require_team_admin)
│ │ ├── api/router.py # Route registration
│ │ ├── core/ # config, database, permissions, security, audit, rate_limit
│ │ ├── models/ # SQLAlchemy models (includes FlowProposal)
│ │ ├── schemas/ # Pydantic schemas
│ │ ── services/psa/ # PSA provider abstraction (base, connectwise/, autotask/, halopsa/, cache, encryption, registry, types)
│ ├── services/knowledge_flywheel.py # AI session analysis → flow proposals
│ ├── services/knowledge_flywheel_scheduler.py # APScheduler job for batch analysis
│ └── services/knowledge_gap_service.py # Weak options & escalation signal detection
│ ├── alembic/ # Database migrations (001-070 sequential, then hash IDs)
│ ├── scripts/ # seed_data.py, seed_trees.py
│ └── tests/ # pytest integration tests
│ │ ├── main.py # FastAPI entry
│ │ ├── api/endpoints/ # auth, trees, sessions, admin, steps, survey, copilot, assistant_chat, integrations, flow_proposals, flowpilot_analytics
│ │ ├── api/deps.py # auth deps (incl. require_team_admin)
│ │ ├── api/router.py # registration
│ │ ├── core/ # config, database, permissions, security, audit, rate_limit
│ │ ├── models/ # SQLAlchemy (incl. FlowProposal)
│ │ ├── schemas/ # Pydantic
│ │ ├── services/psa/ # PSA provider pattern (base, connectwise/, autotask/, halopsa/, cache, encryption, registry, types)
│ │ ├── services/knowledge_flywheel.py + _scheduler.py
│ │ ── services/knowledge_gap_service.py
│ ├── alembic/versions/ # 001-070 sequential, then hex hash
│ ├── scripts/ # seed_data, seed_trees, seed_test_users
│ └── tests/ # pytest integration
├── frontend/
│ ├── src/
│ │ ├── api/ # Axios client + endpoint modules
│ │ ├── components/ # common, layout, dashboard, tree-editor, session, procedural, procedural-editor, library, step-library, ui, flowpilot
│ │ ├── hooks/ # usePermissions, useSessionTimer, useKeyboardShortcuts
│ │ ├── pages/ # All page components
│ │ ├── store/ # Zustand stores (auth, treeEditor, proceduralEditor, userPreferences, scriptGeneratorStore)
│ │ └── types/ # TypeScript interfaces
│ └── (Tailwind v4: CSS-only config in src/index.css)
├── docs/plans/archive/ # Archived design/impl docs (pre-March 2026)
├── CLAUDE.md # This file
├── CURRENT-STATE.md # Detailed feature status
├── LESSONS-LEARNED.md # (Deprecated — consolidated into CLAUDE.md)
└── docs/plans/ # Design docs & implementation plans
│ │ ├── api/ # Axios client + endpoint modules
│ │ ├── components/ # common, layout, dashboard, tree-editor, session, procedural, procedural-editor, library, step-library, ui, flowpilot
│ │ ├── hooks/ # usePermissions, useSessionTimer, useKeyboardShortcuts
│ │ ├── pages/
│ │ ├── store/ # Zustand (auth, treeEditor, proceduralEditor, userPreferences, scriptGeneratorStore)
│ │ └── types/
│ └── (Tailwind v4 CSS-only config in src/index.css)
├── docs/plans/archive/ # pre-March 2026 plans
├── docs/connectwise/ # CW API reference + best-practices guides
├── docs/LESSONS-ARCHIVE.md # archived lessons (fixes in code)
├── CLAUDE.md · CURRENT-STATE.md · DESIGN-SYSTEM.md · DEV-ENV.md
```
---
## Environment Variables
## Design system
### Backend (`backend/.env`)
**Source of truth: [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md).** Read before any visual change.
- Flat high-contrast dark theme, Sentry/PostHog-inspired. **No** glass, backdrop blur, ambient orbs, gradient surfaces.
- Accent **electric blue** (#60a5fa dark / #2563eb light) — ≤5% of UI, interactive elements only. Warning amber (#fbbf24), info cyan (#67e8f9), success green (#34d399), danger red (#f87171). Each with `-dim` at 10% opacity.
- Backgrounds: `bg-sidebar` (#0e1016) → `bg-page` (#16181f) → `bg-card` (#1e2028) → `bg-elevated` (#2a2d38). Borders `border-default` / `border-hover`.
- Text: `text-heading``text-primary``text-muted-foreground``text-muted`.
- Fonts: IBM Plex Sans (body), Bricolage Grotesque (heading, 700 weight for logo), JetBrains Mono (code).
- Logo: 30px gradient square (ember orange) + "ResolutionFlow" in Bricolage Grotesque. Assets in `brand-assets/`, `frontend/src/assets/brand/`, `frontend/public/icons/`.
- Mockups: `docs/mockups/` (HTML).
- **Deprecated — do not use:** glass-card, glass-stat, `bg-gradient-brand`, `backdrop-filter: blur()`, ambient orbs, purple gradients, ember orange as accent, cyan as accent (cyan is info only).
---
## ConnectWise PSA
Reference: `docs/connectwise/` — start with `CONNECTWISE-API-REFERENCE.md`, then the `best-practices/` guides. Extracted OpenAPI spec in `connectwise-psa-resolutionflow-reference.json` (670 endpoints, v2025.16); full spec in `connectwise-psa-openapi-full.json`.
- **Auth:** API Key (Base64 `companyId+publicKey:privateKey`) + `clientId` header every request. `clientId` is server-side (`CW_CLIENT_ID` in `config.py`) — identifies ResolutionFlow, not per-tenant. Per-connection: `company_id`, `public_key`, `private_key`, `server_url`.
- **Architecture:** `services/psa/` provider pattern — `PSAProvider` base, `ConnectWiseProvider` impl, `PsaProviderRegistry` for multi-PSA dispatch. Credentials encrypted at rest via `services/psa/encryption.py` (Fernet). Per-team credentials, never per-user. Endpoints in `api/endpoints/integrations.py`. In-memory TTL cache in `services/psa/cache.py`.
- **Integration flows:** session docs → ticket notes (`POST /service/tickets/{id}/notes`, markdown supported); ticket context → FlowPilot; callbacks via `/system/callbacks` with HMAC verification.
- **API rules:** pin version via Accept header `application/vnd.connectwise.com+json; version=2025.16`. Paginate ≤1000/page. Dynamic base URL via `/login/companyinfo/{companyId}`. Request minimal permissions (MY, not ALL).
---
## Dev commands
Full setup in [DEV-ENV.md](DEV-ENV.md) (host-agnostic, with homelab Proxmox reference topology). Day-to-day:
```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
ResolutionFlow integrates with ConnectWise PSA (formerly Manage) as the primary PSA integration. All ConnectWise API reference materials live in `docs/connectwise/`.
### Best Practices Documentation
Official ConnectWise developer guides live in `docs/connectwise/best-practices/`. Read these BEFORE implementing any CW API integration code:
- `PSA-API-Requests.md` — HTTP methods, response codes, condition query syntax, PATCH format, URL encoding, partial responses, custom fields. READ FIRST.
- `PSA-Callbacks.md` — Callback type/level matrix, retry behavior, URL parameter gotcha, HMAC signature verification.
- `PSA-Pagination.md` — Navigable vs Forward-Only pagination, Link headers, while-loop pattern.
- `PSA-Service-Tickets.md` — Ticket field philosophy, recommended field mappings.
- `PSA-Versioning.md` — Pin API version via Accept header. Use `application/vnd.connectwise.com+json; version=2025.16`.
- `PSA-Cloud-URL-Formatting.md` — Dynamic base URL construction via `/login/companyinfo/{companyId}`.
- `Bundled-Requests.md` — Batch multiple API calls into one request via `/system/bundles`.
- `PSA-Markdown.md` — Ticket notes support markdown. Format session documentation output accordingly.
- `PSA-Company-Synchronization.md` — Filter companies by Status/Type for mapping UI.
- `PSA-Data-Protection.md` — Security role model, request minimal permissions (MY not ALL).
### Reference Files (read in this order)
1. `docs/connectwise/CONNECTWISE-API-REFERENCE.md` — Read FIRST. Quick reference covering auth patterns, tiered endpoint map, key field mappings, and integration architecture flows.
2. `docs/connectwise/connectwise-psa-resolutionflow-reference.json` — Extracted OpenAPI 3.0.1 spec (v2025.16) with only the 670 endpoints and 342 schemas relevant to ResolutionFlow. Use for exact field types, request/response shapes, and parameter details.
3. `docs/connectwise/connectwise-psa-openapi-full.json` — Complete ConnectWise PSA OpenAPI spec (1838 endpoints, 842 schemas). Only consult if you need an endpoint outside the extracted subset.
### Integration Architecture
- **Session → Ticket Notes:** Post auto-generated session documentation to ConnectWise tickets as internal analysis notes via `POST /service/tickets/{id}/notes`
- **Ticket Context → Session Runner:** Pull ticket details, company info, and attached configurations to give FlowPilot AI real-world context
- **Callbacks:** Register webhooks via `/system/callbacks` for real-time ticket event notifications to suggest relevant Flows
### 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 the ResolutionFlow app, NOT per-tenant. Per-connection credentials: `company_id`, `public_key`, `private_key`, `server_url`
- All PSA integration code in `services/psa/` — provider pattern with `PSAProvider` abstract base class, `ConnectWiseProvider` implementation, `PsaProviderRegistry` for multi-PSA dispatch
- PSA endpoints in `api/endpoints/integrations.py` — connection CRUD, ticket ops, member mapping
- Credentials encrypted at rest via `services/psa/encryption.py` (Fernet)
- Each MSP tenant provides their own CW credentials — ResolutionFlow stores these per-team, never per-user
- Design for the Autotask integration following the same service layer pattern (future PSA)
- In-memory TTL cache in `services/psa/cache.py` for board/status/priority lookups
- Respect CW API: paginate with max 1000 per page, handle retries gracefully
---
## Development Commands
```powershell
# Start PostgreSQL (run from VPS SSH — docker not available inside code-server, see Lesson 103)
docker start resolutionflow_postgres
# Backend (from backend/)
source venv/bin/activate # Linux/Mac
# .\venv\Scripts\Activate # Windows
uvicorn app.main:app --reload
# Frontend (from frontend/)
npm run dev
# Run tests (from backend/)
pytest --override-ini="addopts="
# First time only: create test database
docker exec -it resolutionflow_postgres psql -U postgres -c "CREATE DATABASE resolutionflow_test;"
# Frontend build (IMPORTANT: stricter than tsc --noEmit — always use as final check)
cd frontend && npm run build
# Database migrations
cd backend && alembic upgrade head
alembic revision --autogenerate -m "Description"
# Sequential 3-digit IDs (001070) were used historically. New migrations use Alembic's default hex hash IDs.
# Do NOT pass --rev-id — let Alembic generate the hash automatically.
# Access PostgreSQL (run from VPS SSH — docker not available inside code-server, see Lesson 103)
docker compose -f docker-compose.dev.yml up -d # start stack
cd backend && source venv/bin/activate && uvicorn app.main:app --reload
cd frontend && npm run dev
pytest --override-ini="addopts=" # tests (first time: CREATE DATABASE resolutionflow_test)
cd backend && alembic upgrade head # migrate
cd backend && alembic revision -m "desc" # manual migration (preferred per Lesson 77)
cd backend && alembic revision --autogenerate -m "desc" # picks up drift; review carefully
cd frontend && npm run build # stricter than tsc --noEmit — final check
cd frontend && npx tsc -b # TS-only check when dist/ has EACCES
docker exec -it resolutionflow_postgres psql -U postgres -d resolutionflow
# Seed data
cd backend && pip install httpx && python -m scripts.seed_trees
# CI/CD debugging
# CI runs on Gitea (gitea.resolutionflow.com), NOT GitHub Actions — gh run list will return nothing useful
# Check CI status at: https://gitea.resolutionflow.com/chihlasm/resolutionflow/actions
# `gh` CLI is still used for GitHub Issues/PRs (mirrored repo), not for CI runs
python -m scripts.seed_trees # seed (from backend/)
```
### URLs
**URLs:** Frontend <http://localhost:5173>, backend <http://localhost:8000>, API docs <http://localhost:8000/api/docs>.
- Frontend: <http://localhost:5173>
- Backend API: <http://localhost:8000>
- API Docs: <http://localhost:8000/api/docs>
**Test users** (all password `TestPass123!`): `admin@resolutionflow.example.com` (super_admin), `teamadmin@resolutionflow.example.com`, `engineer@resolutionflow.example.com`, `pro@resolutionflow.example.com`.
### Test Users (seeded via `scripts/seed_test_users.py`)
**CI:** Gitea (`gitea.resolutionflow.com/chihlasm/resolutionflow/actions`). `gh` CLI works for issues/PRs on the GitHub mirror, but not CI runs.
- All share password: `TestPass123!`
- `admin@resolutionflow.example.com` (super_admin), `teamadmin@resolutionflow.example.com` (team_admin), `engineer@resolutionflow.example.com` (engineer), `pro@resolutionflow.example.com` (solo pro)
**Never pass `--rev-id`** to alembic — let it generate the hex hash.
---
## Critical Lessons Learned
> Lessons 1-40 archived to `docs/LESSONS-ARCHIVE.md` — fixes are baked into the codebase. Consult if you hit a regression.
### Active Lessons (41+)
**41. Assistant chat uses local React state, not Zustand:** `AssistantChatPage.tsx` uses `useState` for `chats`, `messages`, `input`, `loading`. No store.
**42. Public pages use raw `fetch()`, not `apiClient`:** Survey, shared sessions, and no-auth pages use `fetch()` with full URL. `apiClient` requires auth tokens.
**43. Adding new email types:** Add static async method to `EmailService` in `core/email.py`. Fire-and-forget from endpoints (log errors, don't fail).
**44. AI Chat Builder is flow-type-aware:** `ai_chat_service.py` dispatches by `flow_type`. Troubleshooting: `[TREE_UPDATE]` markers. Procedural: `[STEPS_UPDATE]` markers. Both support `[METADATA]`.
**45. Intake form field schema:** Uses `variable_name` and `field_type` (NOT `name` and `type`).
**46. `CreateFlowDropdown` uses `AIPromptDialog`:** Opens prompt modal, starts AI session, generates flow, navigates to editor with `{ state: { aiPanelOpen: true, sessionId } }`.
**47. Editor-Embedded Flow Assist:** `EditorAIPanel` (320px side panel) + `useEditorAI` hook. Ghost nodes use `_suggestion: true` flag. Actions route to model tiers via `settings.get_model_for_action()`. Delta responses use `[DELTA]...[/DELTA]` markers.
**48. Tree orphan validation uses dynamic root ID:** Orphan check compares against `state.treeStructure?.id` (NOT hardcoded `'root'`).
**49. Full-stack features — verify both ends:** Check the full data flow: schema → endpoint → API client → hook → store → UI.
**50. Anthropic SDK retry:** Set `max_retries=1` to fail fast. Default `max_retries=2` can take 3× timeout.
**51. AI model tier routing:** Use `settings.get_model_for_action(action_type)`. Model IDs: use alias form (`claude-sonnet-4-6`).
**52. Mobile scroll-to-top:** Use `ref.current.scrollIntoView()`, not `window.scrollTo()`. Trigger via `useEffect`.
**53. Flex height chain:** Every ancestor must be a flex container for `flex-1` to work. Missing `flex` class collapses React Flow to 0 height.
**54. React Flow CSS in Tailwind v4:** Import in `index.css`, not component JS. Override dark theme using `--xy-*` CSS custom properties.
**55. App shell height chain:** Every wrapper between `.main-content` and canvas needs `flex` + `flex-1` + `min-h-0` or `h-full`.
**56. Railway backend service name is `patherly`:** Production DB name is `railway`. Public Postgres proxy: `interchange.proxy.rlwy.net:45797`.
**57. Node field priority:** `title``question``description``content``label`. See `copilot_service.py`.
**58. `scriptGeneratorStore.generate()` optional param:** Always wrap: `onClick={() => generate()}`, never `onClick={generate}`.
**59. ConnectWise `clientId` is server-side config:** Set in `config.py` as `CW_CLIENT_ID`. Per-connection: `company_id`, `public_key`, `private_key`, `server_url`.
**60. Dockerfile build args for Vite env vars:** Any new `VITE_*` or `VITE_PUBLIC_*` env var must be added as `ARG` + `ENV` in `frontend/Dockerfile` for Railway deploys. Railway env vars are runtime-only unless explicitly passed through as Docker build args. Without this, `import.meta.env.VITE_*` resolves to `undefined` in production builds.
**61. Procedural sessions auto-start on page load:** `ProceduralNavigationPage` calls `startSession()` immediately in `loadTree()` — there is no intake form screen or "Start" button. Variables are filled inline during execution. Troubleshooting flows DO have a start screen with ticket/client fields. Don't write tests or UI that assume a Start button on procedural flows.
**62. Playwright strict mode — scope selectors to avoid ambiguity:** Step titles appear in both the sidebar checklist and main content heading. Use `getByRole('heading', { name })` for the main content, or scope with `page.locator('.animate-scale-in')` for command palette items. `getByText()` frequently matches multiple elements due to the sidebar + main content layout.
**63. Node 20 required for frontend builds:** Vite 7+ requires Node 20.19+. The system Node may be v18; use nvm: `export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20`. For direct binary access without nvm sourcing: `PATH="$HOME/.nvm/versions/node/v20.19.0/bin:$PATH"`.
**64. PostHog product analytics:** Initialized via `PostHogProvider` in `main.tsx` with explicit `posthog.init()` + `client` prop pattern. Event helpers in `lib/analytics.ts` — use `analytics.eventName(props)` to track. `identifyUser()` called in `authStore.fetchUser()`, `resetAnalytics()` on logout. Env vars: `VITE_PUBLIC_POSTHOG_KEY`, `VITE_PUBLIC_POSTHOG_HOST`. Autocapture enabled.
**65. Local Docker Compose uses `resolutionflow` database on port 5433:** Container name is `resolutionflow_postgres`, database is `resolutionflow` (not `patherly`), port mapped to `5433` (not `5432`). The `POSTGRES_PORT` env var controls this. Playwright config defaults must match: `postgresql+asyncpg://postgres:postgres@127.0.0.1:5433/resolutionflow`.
**66. Dev environment runs on Hostinger VPS (46.202.92.250), not localhost:** Code-server runs in Docker on a VPS (previously devserver01/192.168.0.9). Frontend/backend are accessed via `46.202.92.250`, not `localhost`. CORS must include the VPS IP in `CORS_ORIGINS` and `FRONTEND_URL`. Frontend `.env` must set `VITE_API_URL` to the VPS backend URL. See [DEV-ENV.md](DEV-ENV.md) for full setup, Docker config, networking, and known issues.
**67. Tree editor route is `/trees/new`:** NOT `/editor/new`. Check `router.tsx` line 156 for the canonical path. Use `getTreeEditorPath()` from `@/lib/routing` when navigating programmatically.
**68. APScheduler jobs need `max_instances=1`:** Without it, overlapping scheduler runs can process the same records twice (TOCTOU race). Always set `max_instances=1` on interval jobs in `main.py`.
**69. PostgreSQL `func.sum(case(...))` returns `Decimal` via asyncpg:** Cast to `int()` before storing in Pydantic `dict[str, Any]` fields, or JSON serialization may produce unexpected types.
**70. Toast library uses `toast.warning()` not `toast.warn()`:** Import from `@/lib/toast`. Methods: `success`, `error`, `warning`, `info`. See `frontend/src/lib/toast.ts`.
**71. Enhancement/branch_addition proposals cannot be directly approved:** Backend returns 400 — they require `modified_flow_data` via "Edit & Publish" flow. Only `new_flow` proposals support direct approve.
**72. `ai_sessions.status` column is `VARCHAR(30)`:** Must fit `requesting_escalation` (23 chars). If adding new status values, verify length. Migration `f0aad74ea51b` widened from 20→30.
**73. `get_db` rolls back on exception:** The dependency does `await session.rollback()` on error to prevent `InFailedSQLTransaction` cascade. Never remove this — without it, one failed request poisons subsequent requests on the same connection.
**74. FlowPilot action bar height chain:** The action bar (Resolve/Escalate/Pause) requires every ancestor from `app-shell` grid down to have proper flex constraints. Key fix: `ViewTransitionOutlet` wrapper needs `flex flex-col`. If action bar disappears, check height chain with DevTools `getBoundingClientRect()` walk.
**75. Dashboard prefill auto-submits:** `StartSessionInput` navigates to `/pilot` or `/assistant` with `{ state: { prefill } }`. `FlowPilotSessionPage` auto-submits via `useEffect` + `prefillHandledRef` guard — no double-enter. `AssistantChatPage` does the same pattern.
**76. Active session navigation guard:** `FlowPilotSessionPage` uses `useBlocker` (same as `TreeEditorPage`) to intercept navigation during active sessions. "Pause & Leave" auto-pauses before proceeding.
**77. Prefer manual Alembic migrations for targeted changes:** `alembic revision --autogenerate` picks up drift from all tables. 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":** Not "Decision Tree Platform". This tagline appears on login, register, and the HTML `<title>`. The old "Decision Tree Platform" was internal jargon misaligned with user-facing branding.
**79. Custom modals must be mobile-responsive:** Use `items-end sm:items-center` (bottom-sheet on mobile, centered on desktop) and `max-w-full sm:max-w-lg` (full-width on mobile). The shared `Modal.tsx` does this correctly — custom modal implementations must follow the same pattern. See `PrepareSessionModal.tsx` for the fix pattern.
**80. TopBar search collapses to icon on mobile:** Full search bar (`hidden sm:block`) shows on desktop; magnifying glass icon button (`sm:hidden`) shows on mobile (<640px). Both open the same CommandPalette. Don't add `w-full` search bar without the mobile icon fallback.
**81. Never use `transition: all` in landing.css:** Specify exact properties: `transition: background 0.3s, border-color 0.3s, box-shadow 0.3s, transform 0.3s, opacity 0.3s`. `transition: all` animates layout properties and causes jank.
**82. `bun` requires PATH setup on devserver01:** `export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"`. The gstack browse binary and Playwright need this. Chromium system deps: `libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2`.
**83. ~~FlowPilot ActionBar fixed bottom~~ (Superseded by Lesson 93):** Actions moved to the page header. `FlowPilotActionBar` component exists but is no longer used in the main session flow. The only fixed-bottom element is the message input.
**84. AI session `abandoned` status is fully wired:** `POST /ai-sessions/{id}/abandon` sets status to `abandoned` with optional `reason` param. Frontend: `aiSessionsApi.abandonSession()`, `useFlowPilotSession().abandonSession()`, "Close" button in `FlowPilotActionBar`. Redirects to `/sessions` after closing.
**85. Date range filter end dates must use end-of-day:** `toDate.toISOString()` sends midnight (start of day), excluding items created later that day. Always set `toDate.setHours(23, 59, 59, 999)` before sending. For string-based date inputs (AI sessions), append `T23:59:59.999Z`. See `SessionHistoryPage.tsx`.
**86. Script Builder system:** AI-powered script generation at `/script-builder`. Chat-style interface generates PowerShell/Bash/Python scripts from natural language. Backend: `ScriptBuilderSession` model, `script_builder_service.py`, endpoints at `/scripts/builder/`. Frontend: `ScriptBuilderPage`, `ScriptCodeBlock`, `ScriptPreviewModal`, `SaveToLibraryDialog`. FlowPilot can hand off to Script Builder via `action_type: "open_script_builder"` with `sessionStorage` context passing.
**87. FlowPilot must ask GUI vs script preference:** When a task can be done via GUI or script (e.g., creating AD users), FlowPilot must ask the engineer which approach they prefer BEFORE suggesting either. Never assume the user wants a script. See `FLOWPILOT_SYSTEM_PROMPT` rules in `flowpilot_engine.py`.
**88. Charcoal palette — sidebar-darkest approach:** Sidebar `#0e1016`, page `#16181f`, cards `#1e2028`, borders `#2a2e3a`. This gives more contrast range than true-dark. All colors via CSS variables in `index.css` `@theme` block. Accent is electric blue (#60a5fa), not orange or cyan.
*(Lessons 8991 were retracted.)*
**92. `tsc -b` in Dockerfile is stricter than `npx tsc --noEmit`:** The production build (`tsc -b && vite build`) enforces `noUnusedLocals` and `noUnusedParameters` as hard errors. After any refactor that moves logic between components or removes features, trace every import and destructured prop to remove orphans. IDE warnings (yellow squiggles) flag these — check them before pushing.
**93. FlowPilot actions live in the page header, not a bottom bar:** `FlowPilotSessionPage` renders Resolve/Escalate/Share Update in the header bar. Desktop: inline buttons + `⋯` overflow (Pause/Close). Mobile: single `⋯` menu. The bottom only has the message input. `FlowPilotActionBar` component still exists but is no longer used in the main session flow.
**94. Frontend chat uses unified_chat_service, not assistant_chat_service:** `AssistantChatPage` calls `/ai-sessions/{id}/chat``unified_chat_service.py`. The old `assistant_chat_service` endpoints were removed (only retention settings remain at `/assistant/retention`). When tracing chat features, start from `aiSessionsApi.sendChatMessage``ai_sessions.py``unified_chat_service.py`. Never wire chat features into `assistant_chat.py`.
**95. Image upload → AI vision pipeline:** Paste/attach images → upload to Railway S3 bucket via `uploadsApi.upload()` → send `upload_ids` with chat message → backend fetches from S3 via `storage_service.download_file()` → resized via `storage_service.resize_image_for_vision()` (Pillow, 1568px max, PNG→JPEG) → base64-encoded → sent as Claude multimodal content blocks. Max 3 images/message. Images are NOT stored in conversation history (text-only). Vision helpers live in `storage_service.py`.
**96. `bg-accent` is electric blue — never use for code/kbd elements:** In Tailwind v4, `bg-accent` maps to `--color-accent: #60a5fa` (dark) / `#2563eb` (light). Use `bg-code` for code blocks, `bg-white/[0.12] border border-white/[0.06]` for inline code/badges, `bg-white/[0.08]` for kbd shortcuts. Blue accent is reserved for interactive elements only (buttons, active nav, links). Ember orange (#f97316) is deprecated — do not use.
**97. Railway Object Storage (S3 bucket) is provisioned:** Bucket `resolutionflow-uploads` on Railway canvas. Variables: `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY`, `STORAGE_BUCKET_NAME`, `STORAGE_REGION` — mapped via variable references on the `patherly` backend service. Accessed via boto3 in `storage_service.py`. Pillow (`Pillow>=10.0.0`) + `libjpeg-dev`/`zlib1g-dev` in Dockerfile for image resize.
**98. `lazyWithRetry` for stale chunk errors:** All lazy-loaded routes use `lazyWithRetry` from `@/lib/lazyWithRetry.ts` instead of `React.lazy`. Auto-reloads the page on chunk load failures (stale deploys). Uses sessionStorage debounce (10s) to prevent loops. When adding new lazy routes, use `lazyWithRetry`, not `lazy`.
**99. Tailwind v4 `text-secondary` renders invisible on dark backgrounds:** `text-secondary` maps to `--color-secondary: #2e3140` (a dark surface color), NOT `--color-text-secondary`. For readable secondary text, use `text-muted-foreground` (`#848b9b`). Also avoid `text-muted` (`#4f5666`) for body text — it's for labels only. This applies to ALL new components.
**100. Hover pop-out card pattern:** For cards that expand on hover "in front of everything": use `pointer-events-none` on the scrim (`fixed inset-0 z-40 bg-black/30`), absolute-position the expanded card at `z-50` with its own `onClick` handler, and dismiss via `onMouseLeave` on the wrapper div. Never put interactive event handlers on the scrim — it blocks clicks on sibling elements.
**101. AI marker format compliance:** The AI assistant uses `[QUESTIONS]`, `[ACTIONS]`, and `[FORK]` markers in responses. Parsed by `unified_chat_service.py` (`_parse_*_marker` functions), returned as structured data in the API response. System prompt in `assistant_chat_service.py` has a final reminder section, and each user message gets an invisible `[SYSTEM: ...]` reminder appended in `_call_anthropic_cached()`. If markers stop appearing: check conversation history stores `display_content` (stripped), verify system prompt final reminder exists, check user message reminder injection is active.
**102. TaskLane activation must happen in ALL chat response paths:** `AssistantChatPage.tsx` has three code paths calling `sendChatMessage`: `handleSend` (regular messages), `sendPrefill` (dashboard handoff), `handleResumeNew` (resume from concluded session). ALL three must check `response.actions`/`response.questions` and call `setShowTaskLane(true)`. Missing this in any path causes TaskLane to not appear on first message.
**103. Docker not available in code-server container:** The dev environment runs code-server inside Docker on the VPS. The `docker` CLI is not available inside the code-server container. To query the database, use the VPS SSH session: `docker exec resolutionflow_postgres psql -U postgres -d resolutionflow -t -c "SQL"`. Python is also not available in the container.
**104. `landing.css` uses self-contained `--lp-*` color variables:** The landing page defines its own color palette at the top of `landing.css` (`--lp-bg`, `--lp-accent`, `--lp-text-*`, etc.). Never use `var(--color-*)` theme tokens in `landing.css` — they may resolve incorrectly outside the app shell context. Extend the `--lp-*` palette for any new landing page colors.
**105. `npm run build` fails with `EACCES: permission denied` on `dist/` in code-server:** This is a filesystem permission issue in the Docker environment, not a TypeScript error — the TS compilation completes successfully. Use `npx tsc -b` to verify TypeScript cleanly without needing to write to `dist/`.
**106. Guard async "select item → load data → apply state" flows with a ref:** When a component lets the user switch between items (chat sessions, flows, scripts) and loads data asynchronously on each switch, the load for item A can complete *after* the user has already switched to item B — overwriting B's state with A's stale data. Fix pattern: keep a `currentSelectionRef = useRef(initialId)` and update it synchronously whenever the selection changes (in every creation/switch path). After every `await`, bail out if `currentSelectionRef.current !== thisItemId`. See `AssistantChatPage.tsx` `selectChat` for the reference implementation (`currentChatRef`).
**107. Startup routines must use `_admin_session_factory()` after Phase 4 RLS:** Any code that runs at startup (lifespan, `ensure_service_account`, seed scripts) and touches tenant-isolated tables (`users`, etc.) must use `_admin_session_factory()` — not `get_db()`. Phase 4 enabled RLS on `users`; a tenant-scoped session has no `app.current_account_id` set at startup, so all queries return 0 rows or fail. `get_service_account_id` in `deps.py` is safe — it reads from `app.state` cached at startup, never hits the DB per-request.
**108. Tables with no `account_id` column (never add to RLS migrations):** `script_categories`, `platform_steps`, `template_trees`, `plan_feature_defaults`, `accounts` — global/platform tables documented with "No account_id. No RLS." in their model files. When writing RLS migrations, scan at the class level (check for `account_id: Mapped` within the class block), not the file level — multiple classes in one `.py` file can have different columns (e.g. `ScriptCategory` vs `ScriptTemplate` in `script_template.py`).
**109. `tree_shares.account_id` must equal `tree.account_id`, not the actor's account:** When creating a `TreeShare`, always use `account_id=tree.account_id` (tree owner's tenant). A super admin in tenant A sharing tenant B's tree must produce a share row in tenant B's RLS context — using `current_user.account_id` instead makes the share invisible to the tree owner after RLS is enforced.
**110. Backfill migrations for `account_id` require a service-code audit:** When a migration adds `account_id` to an existing model via backfill (nullable → backfill → NOT NULL), grep for ALL `ModelClass(` instantiation sites in service code and verify `account_id=` is passed. SQLAlchemy accepts `None` silently with no warning; Phase 4 RLS WITH CHECK only surfaces the problem at runtime as `InsufficientPrivilegeError: new row violates row-level security policy`. Fixed example: `AISessionStep` — all 5 creation sites in `flowpilot_engine.py` were missing `account_id` until April 2026.
**111. Global Axios interceptor fires before component `.catch()` — fix optional-data endpoints at the source:** The global 5xx handler in `client.ts` fires for ALL non-401 5xx responses, even when a component does `.catch(() => {})`. If an endpoint returns optional UI data (e.g., board filters, PSA config), return `[]` / `{}` on provider failure rather than raising 502. Silencing the error in the component is not enough — the toast appears anyway. See `list_boards` in `integrations.py` for the fixed pattern.
## 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(user, db)` (any active + auto-downgrades expired trials), `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`
## Common tasks
- **New endpoint:** `endpoints/``router.py``schemas/` → tests → frontend API client.
- **New page:** `pages/` → route in `router.tsx` → nav in `AppLayout.tsx`.
- **New public route:** top-level in `router.tsx` alongside `/login`, not inside `ProtectedRoute`.
- **New frontend API module:** types in `types/` → export from `types/index.ts` → client in `api/` → export from `api/index.ts`.
- **Schema change:** update model → `alembic revision -m "desc"` → review → `alembic upgrade head`.
- **New `VITE_*` env var:** add as `ARG` + `ENV` in `frontend/Dockerfile` for Railway builds (Lesson 60 — Railway env vars are runtime-only, Vite bakes at build time).
- **Account sub-page:** add route in `router.tsx` under `account` children + add link card in `AccountSettingsPage.tsx``AccountLayout` has NO sidebar nav.
---
## Design System
## Coding standards
**Source of truth:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — always read this before making visual or UI decisions.
- **Python:** type hints everywhere, async/await for DB, Pydantic v2, `DateTime(timezone=True)` always.
- **TypeScript:** interfaces for all data, `const` over `let`, functional components + hooks, shared logic in custom hooks.
- **Git:** feature branch before committing (`git checkout -b feat/feature-name`). Format: `type: description` (feat/fix/refactor/docs/test/chore). Always `Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>`. Large features: commit per phase with `npm run build` validation. Push to Gitea — auto-mirrors to GitHub (`.gitea/workflows/mirror-to-github.yml`); never push GitHub directly.
- **Theme:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. Light mode fully specified (v6).
- **Backgrounds:** `bg-page` (`#16181f`), `bg-sidebar` (`#0e1016`), `bg-card` (`#1e2028`), `bg-elevated` (`#2a2d38`)
- **Cards:** `bg-card` with 1px `border-default` (`#2a2e3a`), 8px radius. No shadows, no blur, no gradients. Hover: `border-hover` (`#3d4252`)
- **Buttons:** Primary: solid `accent` (#60a5fa dark / #2563eb light), white text, 5px radius. Ghost: transparent + 1px border, hover `bg-elevated`
- **Inputs:** `bg-input` (`#252830`) with 1px `border-default`, 5px radius. Focus: `border-color: accent` + `box-shadow: 0 0 0 2px accent-dim`
- **Text:** `text-heading` (`#f0f2f5`) → `text-primary` (`#e2e5eb`) → `text-muted-foreground` (`#848b9b`) → `text-muted` (`#4f5666`). NEVER use `text-secondary` — in Tailwind v4 it maps to a surface color, not a text color.
- **Borders:** `border-default` (`#2a2e3a`), `border-hover` (`#3d4252`)
- **Functional colors:** `#34d399` (success), `#fbbf24` (warning/amber), `#f87171` (danger), `#67e8f9` (info/cyan) — each with `-dim` variant at 10% opacity
- **Accent:** Electric blue `#60a5fa` (dark) / `#2563eb` (light) — used sparingly (≤5% of UI). `accent-dim` = `rgba(96,165,250,0.10)`, `accent-text` = `#93c5fd`
- **Deprecated:** Do NOT use `glass-card`, `glass-stat`, `bg-gradient-brand`, `text-gradient-brand`, `backdrop-filter: blur()`, ambient orbs, purple gradients, ember orange (`#f97316`), or cyan (`#22d3ee`) as accent — cyan is now the info color only
**After shipping:** update `CURRENT-STATE.md` + `03-DEVELOPMENT-ROADMAP.md`, `gh issue close #N` for resolved issues, add lessons here only for non-obvious traps (otherwise let the code speak).
---
## Frontend Patterns
## 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'`
- **Scratchpad overlay:** `position: fixed`, `onOpenChange` callback for parent padding adjustment, `right-2` positioning
- **Custom step flow:** `CustomStepModal``PostStepActionModal``ContinuationModal` → custom step view. Key state: `pendingStep`, `pendingContinuationNodeId`, `customBranchMode`, `branchOriginNodeId`. Use `findCustomStep()` not `findNode()` for custom step UUIDs.
- **Session sharing:** `ShareSessionModal` manages share links, `SharedSessionPage` renders public/account views. Helper utils in `lib/sessionShare.ts`. Share URLs use `/shared/sessions/:token`.
- **Procedural navigation:** `ProceduralNavigationPage` handles intake forms, step-by-step execution, and resume via `location.state.sessionId`. Uses `StepChecklist`, `StepDetail`, `ProgressBar`, `CompletionSummary` components.
- **Routing helper:** Use `getTreeNavigatePath()` and `getTreeEditorPath()` from `@/lib/routing` for all tree/session navigation.
- **Account section layout:** `AccountLayout` has NO sidebar nav. Account sub-pages (categories, target-lists) are reached via link cards on `AccountSettingsPage.tsx`. New account pages: add route in `router.tsx` under `account` children + add a link card in `AccountSettingsPage`.
- **Dashboard cockpit:** `QuickStartPage` is the copilot-first launchpad. Greeting + "What are you troubleshooting?" + ChatGPT-style `StartSessionInput` (auto-growing textarea, paste images, drag-drop files, attach button, paste logs, suggestion chips). Below: `PendingEscalations`, `ActiveFlowPilotSessions`, `RecentFlowPilotSessions`. Collapsible "Dashboard" section for `PerformanceCards`, `KnowledgeBaseCards`, `TeamSummary`.
- **Sidebar sections:** Amber "New Session" button → Home → RESOLVE (History) → KNOWLEDGE (Flows with Solutions Library sub-item, Scripts) → INSIGHTS (Data). Footer: Account, Pin/Unpin. No help/guides/feedback in sidebar — accessible via TopBar.
- **Component basics:** `cn()` from `@/lib/utils`, Lucide icons, `Modal.tsx` for modals (mobile-responsive `items-end sm:items-center` + `max-w-full sm:max-w-lg`).
- **Types:** Create in `types/`, export from `types/index.ts`, `import type { T } from '@/types'`.
- **Routing:** `getTreeNavigatePath()` / `getTreeEditorPath()` from `@/lib/routing`. Tree editor is `/trees/new`. All dashboard session clicks → `/pilot/:id` regardless of `session_type`.
- **Lazy routes:** `lazyWithRetry` from `@/lib/lazyWithRetry.ts`, not `React.lazy` (auto-reload on stale chunks).
- **Public pages:** raw `fetch()` with full URL, NOT `apiClient` (which requires auth tokens).
- **Toast:** `toast.warning()` not `toast.warn()`. Import from `@/lib/toast` — methods: `success`, `error`, `warning`, `info`.
- **Assistant chat:** uses local React `useState`, not Zustand. All three send paths (`handleSend`, `sendPrefill`, `handleResumeNew`) must call `setShowTaskLane(true)` when response has actions/questions.
- **Chat backend wiring:** `aiSessionsApi.sendChatMessage``/ai-sessions/{id}/chat``unified_chat_service.py`. NOT `assistant_chat_service.py` (removed except retention settings).
- **FlowPilot:** Actions live in page header (Resolve/Escalate/Share Update + overflow). `useBlocker` for active-session nav guard. "Pause & Leave" auto-pauses.
- **AI markers:** `[QUESTIONS]`, `[ACTIONS]`, `[FORK]`, `[DELTA]...[/DELTA]` (editor), `[TREE_UPDATE]` (troubleshooting builder), `[STEPS_UPDATE]` (procedural builder), `[METADATA]`. Parsed in `unified_chat_service.py`; conversation history stores stripped `display_content`. If markers disappear: check system-prompt final reminder + per-user-message `[SYSTEM: ...]` injection in `_call_anthropic_cached()`.
- **Image uploads:** paste/attach → Railway S3 via `uploadsApi.upload()` → resized by `storage_service.resize_image_for_vision()` (Pillow, 1568px max, PNG→JPEG) → base64 → Claude multimodal blocks. Max 3/msg. Images NOT stored in history.
- **Async select-load-apply:** guard with a ref (pattern in `AssistantChatPage` `currentChatRef`). Update synchronously on every selection change; after every `await`, bail out if `ref.current !== thisId`.
- **Editor-Embedded Flow Assist:** `EditorAIPanel` (320px side panel) + `useEditorAI`. Ghost nodes via `_suggestion: true`. Route actions via `settings.get_model_for_action()`.
- **Script Builder:** `/script-builder`, chat-style. Backend `ScriptBuilderSession`, `script_builder_service.py`, endpoints `/scripts/builder/`. FlowPilot handoff via `action_type: "open_script_builder"` + `sessionStorage`.
- **Intake form field schema:** `variable_name` + `field_type` (NOT `name` / `type`).
- **Node field priority** (copilot, summaries): `title``question``description``content``label`.
- **Procedural sessions auto-start** on page load (no intake/Start screen). Troubleshooting flows DO have a start screen.
---
## Common Tasks
## Critical lessons
- **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`
- **New public route (no auth):** Add at top level in `router.tsx` alongside `/login`, `/register` — NOT inside the `ProtectedRoute`/`AppLayout` children.
- **Schema change:** Update model → `alembic revision --autogenerate -m "desc" --rev-id=NNN` (NNN = next sequential number, e.g., 068 → 069) → review → `alembic upgrade head`
- **New frontend API module:** Types in `types/` → export from `types/index.ts` → client in `api/` → export from `api/index.ts`
> Lessons 1-40 archived to `docs/LESSONS-ARCHIVE.md` — fixes baked into the codebase. **Grep the archive when an error message or symptom is unfamiliar, or after two failed attempts at resolving an issue.** Don't pre-load for routine work.
### Backend / data
- **APScheduler interval jobs always `max_instances=1`** — without it, overlapping runs reprocess records (TOCTOU).
- **`get_db` rolls back on exception** — never remove the `await session.rollback()`, or one failed request poisons the connection with `InFailedSQLTransaction` cascading.
- **Startup routines on tenant-isolated tables must use `_admin_session_factory()`, not `get_db()`.** Phase 4 RLS has no `app.current_account_id` set at startup. `get_service_account_id` is safe (reads cached `app.state`).
- **Backfill migrations adding `account_id`:** grep ALL `ModelClass(` sites in service code to verify `account_id=` is passed. SQLAlchemy accepts `None` silently — Phase 4 RLS WITH CHECK surfaces the problem at runtime as `InsufficientPrivilegeError: new row violates row-level security policy`.
- **`tree_shares.account_id = tree.account_id`**, never `current_user.account_id`. A super_admin sharing another tenant's tree must produce the share in the tree owner's tenant, or it becomes invisible post-RLS.
- **Global tables (no `account_id`, never in RLS migrations):** `script_categories`, `platform_steps`, `template_trees`, `plan_feature_defaults`, `accounts`. Scan at class level — one `.py` file can hold multiple classes with different columns (e.g. `ScriptCategory` vs `ScriptTemplate`).
- **`ai_sessions.status` is VARCHAR(30)** — fits `requesting_escalation` (23 chars). Migration `f0aad74ea51b` widened from 20.
- **PostgreSQL `func.sum(case(...))` returns `Decimal` via asyncpg** — cast to `int()` before Pydantic `dict[str, Any]`.
- **Enhancement / branch_addition proposals need `modified_flow_data` via "Edit & Publish"** — backend 400 on direct approve. Only `new_flow` supports direct approve.
- **Adding email types:** static async method on `EmailService` in `core/email.py`. Fire-and-forget from endpoints (log errors, don't fail the request).
### AI / FlowPilot
- **Anthropic SDK `max_retries=1`** — default of 2 can take 3× the timeout.
- **Model tier routing:** `settings.get_model_for_action(action_type)`. Always alias form (`claude-sonnet-4-6`).
- **FlowPilot must ask GUI-vs-script before suggesting either** when both are viable — see `FLOWPILOT_SYSTEM_PROMPT` in `flowpilot_engine.py`.
- **Telemetry events to grep:** `anthropic.cache` (prompt-cache hit/create), `mcp.turn` (per-turn MCP availability), `mcp.fallback` (MCP silent-retry fired).
### Frontend / UI
- **Flex height chain:** every ancestor from `app-shell` grid to React Flow canvas needs `flex` + `flex-1` + `min-h-0` or `h-full`. Missing `flex` collapses to 0. Same rule for FlowPilot action bar and any tall scroller.
- **React Flow CSS in Tailwind v4:** import in `index.css`, not component JS. Override dark theme via `--xy-*` CSS vars.
- **`text-secondary` renders invisible on dark** — Tailwind v4 maps it to `--color-secondary` (a surface color). Use `text-muted-foreground` for readable secondary text. Avoid `text-muted` for body — labels only.
- **`bg-accent` is electric blue — never for code/kbd.** Use `bg-white/[0.12] border border-white/[0.06]` for inline code, `bg-white/[0.08]` for kbd. Accent reserved for interactive elements.
- **`landing.css` uses self-contained `--lp-*` vars** — never `var(--color-*)` theme tokens (they resolve incorrectly outside the app shell).
- **Never `transition: all`** — list properties explicitly, or layout props animate and jank.
- **Date range filter end dates:** `setHours(23, 59, 59, 999)` before sending, or the day's items are excluded. For string-based date inputs, append `T23:59:59.999Z`.
- **TopBar search:** full bar `hidden sm:block`, icon button `sm:hidden` — both open CommandPalette.
- **Hover pop-out cards:** scrim `pointer-events-none`, expanded card has its own click handler at `z-50`, dismiss via `onMouseLeave` on wrapper. Never put handlers on the scrim.
- **`tsc -b` in Dockerfile is stricter than `tsc --noEmit`** — enforces `noUnusedLocals` / `noUnusedParameters` as hard errors. Check IDE yellow squiggles before pushing.
- **Dashboard prefill auto-submits** via `useEffect` + `prefillHandledRef` guard — no double-enter.
- **Global Axios 5xx interceptor fires before component `.catch()`** — fix optional-data endpoints at the source (return `[]` / `{}` on provider failure), not in the component.
- **Playwright strict mode:** scope selectors to avoid sidebar/main ambiguity. Use `getByRole('heading', { name })` or `.animate-scale-in` locators, not bare `getByText()`.
### Env / infra
- **Node 20.19+ required** (Vite 7). `nvm use 20` or `PATH="$HOME/.nvm/versions/node/v20.19.0/bin:$PATH"`.
- **Railway backend service is `patherly`, DB name `railway`.** Public Postgres proxy: `interchange.proxy.rlwy.net:45797`.
- **Railway Object Storage bucket `resolutionflow-uploads`.** Env vars `STORAGE_*`. boto3 in `storage_service.py`. Dockerfile needs Pillow + `libjpeg-dev` / `zlib1g-dev`.
- **PostHog:** `PostHogProvider` + `posthog.init()` in `main.tsx`. Helpers in `lib/analytics.ts`. Env: `VITE_PUBLIC_POSTHOG_KEY`, `VITE_PUBLIC_POSTHOG_HOST`. `identifyUser()` in `authStore.fetchUser()`, `resetAnalytics()` on logout.
- **bun PATH on devserver01:** `BUN_INSTALL="$HOME/.bun"`, `PATH="$BUN_INSTALL/bin:$PATH"`. Playwright Chromium needs `libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2`.
- **Full-stack change:** trace schema → endpoint → API client → hook → store → UI. Don't assume one end proves the other.
- **Dev env** — see DEV-ENV.md for current topology, `REPO_ROOT` requirement when compose runs inside a container, Vite `allowedHosts`, linuxserver.io `group_add` + custom-cont-init.d workaround, `docker compose up` no-op-on-unchanged-hash gotcha.
---
## Coding Standards
## GitNexus code intelligence
### Python
Indexed as `resolutionflow`. Earns its cost on cross-cutting work only.
- Type hints everywhere, async/await for DB, Pydantic for validation, `DateTime(timezone=True)` always
| Tool | When |
|---|---|
| `gitnexus_query({query})` | Find code by concept when you don't know where to look |
| `gitnexus_context({name})` | Callers/callees of a symbol before touching it |
| `gitnexus_impact({target, direction})` | Blast radius before editing shared symbols |
| `gitnexus_rename({symbol_name, new_name, dry_run: true})` | Safe multi-file rename |
### TypeScript
**Use for:** core shared symbols (`flowpilot_engine`, `unified_chat_service`, auth middleware, `get_db`, shared hooks), cross-file renames, unfamiliar bug traces, refactor safety. **Skip for:** new endpoints, isolated fixes, changes you can read in one file.
- Interfaces for all data, `const` over `let`, functional components + hooks, reusable logic in custom hooks
### Git
- Format: `type: description` (feat, fix, refactor, docs, test, chore)
- Always include `Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>`
- Always create feature branch BEFORE committing: `git checkout -b feat/feature-name`
- Large features: commit per phase with `npm run build` validation
- **Remote is Gitea, not GitHub directly:** Push to `gitea.resolutionflow.com/chihlasm/resolutionflow`. Gitea auto-mirrors to GitHub via `.gitea/workflows/mirror-to-github.yml` — never push directly to GitHub.
### After Completing Work
When a feature, fix, or significant piece of work is finished and merged/committed:
1. **Update `CURRENT-STATE.md`** — move completed items, update "In Progress" and "What's Next" sections
2. **Update `03-DEVELOPMENT-ROADMAP.md`** — check off completed work, update phase status
3. **Close related GitHub Issues** — use `gh issue close #N` for any issues resolved by the work
4. **Update `CLAUDE.md`** if the work introduced new patterns, lessons learned, or changed project structure
Re-indexes automatically on commit (PostToolUse hook). Manual refresh if stale: `npx gitnexus analyze`.
---
## gstack (Browser & Workflow Skills)
## gstack skills
**Web browsing:** Always use the `/browse` skill from gstack for all web browsing needs. Never use `mcp__claude-in-chrome__*` tools.
Always use `/browse` for web, never `mcp__claude-in-chrome__*`. Most-used:
**Available skills:**
| Skill | Purpose |
|-------|---------|
| `/office-hours` | Brainstorm new ideas (YC-style office hours) |
| `/plan-ceo-review` | CEO/founder-mode plan review (scope, ambition) |
| `/plan-eng-review` | Engineering plan review (architecture, edge cases) |
| `/plan-design-review` | Design plan review (UI/UX critique) |
| `/design-consultation` | Create a design system / DESIGN.md |
| `/review` | Pre-landing PR code review |
| `/ship` | Ship workflow (tests, review, PR creation) |
| `/browse` | Headless browser for QA testing and site dogfooding |
| `/qa` | Systematic QA testing + auto-fix bugs found |
| `/qa-only` | QA report only (no fixes) |
| `/design-review` | Visual QA — find and fix design inconsistencies |
| `/setup-browser-cookies` | Import cookies from real browser for authenticated testing |
| `/retro` | Weekly engineering retrospective |
| `/investigate` | Systematic debugging with root cause analysis |
| `/document-release` | Post-ship documentation updates |
| `/codex` | Second opinion via OpenAI Codex CLI |
| `/careful` | Safety guardrails for destructive commands |
| `/freeze` | Restrict edits to a specific directory |
| `/guard` | Full safety mode (careful + freeze) |
| `/unfreeze` | Remove edit restrictions |
| `/gstack-upgrade` | Upgrade gstack to latest version |
- `/review` — pre-land PR review
- `/ship` — tests + review + PR creation
- `/browse` + `/qa` / `/qa-only` — headless browser testing (setup: Lesson 82)
- `/design-review` — visual QA
- `/investigate` — systematic debug with root cause
- `/codex` — OpenAI Codex second opinion
- `/plan-eng-review` / `/plan-design-review` / `/plan-ceo-review` — plan critiques
---
## Deployment (Railway)
- **Production:** `resolutionflow.com` (frontend), `api.resolutionflow.com` (backend)
- Auto-deploys via: push to Gitea → Gitea mirrors to GitHub → Railway watches GitHub `main` and deploys
- PR environments auto-created (need manual domain generation in Railway dashboard)
- PR envs need `VITE_API_URL` set with `https://` prefix on frontend service
- `ALLOW_RAILWAY_ORIGINS=true` enables CORS for `*.up.railway.app`
- Shared Variables (project-level in Railway dashboard) auto-propagate to all environments including PR envs — use for secrets like `ANTHROPIC_API_KEY`
- Super admin utility: `backend/make_superadmin_simple.py list|<email>`
- **Prod:** `resolutionflow.com` (frontend), `api.resolutionflow.com` (backend).
- Auto-deploy: Gitea push → GitHub mirror → Railway follows GitHub `main`.
- PR environments auto-created; need manual domain generation + `VITE_API_URL` with `https://` prefix.
- `ALLOW_RAILWAY_ORIGINS=true` for `*.up.railway.app` CORS.
- Shared Variables (Railway project-level) auto-propagate to PR envs — use for secrets like `ANTHROPIC_API_KEY`.
- Super admin utility: `backend/make_superadmin_simple.py list|<email>`.
---
## Future Roadmap
- **Phase 3:** PSA integrations (ConnectWise in progress), file attachments, client context, analytics
- **Phase 4:** Additional PSA integrations (Autotask/Kaseya), PowerShell automation, enterprise SSO
---
## Quick Reference
## 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` |
| Bugs & Fixes | CLAUDE.md → Critical Lessons Learned section |
| Design System | [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) |
| Dev Environment | [DEV-ENV.md](DEV-ENV.md) — 46.202.92.250 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 -->
|---|---|
| Detailed status | [CURRENT-STATE.md](CURRENT-STATE.md) |
| Roadmap | [03-DEVELOPMENT-ROADMAP.md](03-DEVELOPMENT-ROADMAP.md) |
| Design system | [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) |
| Dev env | [DEV-ENV.md](DEV-ENV.md) |
| Archived lessons | [docs/LESSONS-ARCHIVE.md](docs/LESSONS-ARCHIVE.md) |
| ConnectWise API | `docs/connectwise/` |
| GitHub issues | `gh issue list --state open` |
| Local API docs | <http://localhost:8000/api/docs> |

View File

@@ -83,7 +83,7 @@ DEV_HOST = <hostname or IP your browser uses, e.g. dev.internal, 10
DEV_HOST_SCHEME = <http or https; http is fine for internal dev, https if behind a TLS proxy>
FRONTEND_PORT = 5173
BACKEND_PORT = 8000
POSTGRES_PORT = 5432 # or 5433 if you're avoiding conflict with a host Postgres
POSTGRES_PORT = 5433 # host-side port. 5433 is the recommended default on any shared host to avoid collision with a host-level Postgres. The container's internal port stays 5432.
POSTGRES_DB_NAME = resolutionflow
POSTGRES_USER = postgres
POSTGRES_PASSWORD = <local-dev-password; anything, this is not prod>
@@ -149,7 +149,7 @@ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
# Adjust pg_hba.conf if you need non-local connections.
```
**Option B (Postgres via Docker Compose):** The repo has a `docker-compose.dev.yml` at the root. Check its Postgres service for the container name, port mapping, and volume. CLAUDE.md Lesson 65 notes the local compose defaults use container name `resolutionflow_postgres`, database `resolutionflow`, port `5433` mapped to the host. Confirm what the compose file actually says on your branch before trusting those values.
**Option B (Postgres via Docker Compose):** The repo has a `docker-compose.dev.yml` at the root. Check its Postgres service for the container name, port mapping, and volume. The local compose defaults use container name `resolutionflow_postgres`, database `resolutionflow`, and host-side port `5433` (mapped to the container's internal `5432`) — see CLAUDE.md Lesson 65. The host-side `5433` is the recommended default on any shared host: it keeps the port free for a host-level Postgres if you ever need one. The compose file also defines explicit `command:` directives on both `backend` and `frontend` to force `--host 0.0.0.0`, and expects the caller to pass `REPO_ROOT` (see 5.4) for bind-mount resolution. Confirm what the compose file actually says on your branch before trusting these values.
```bash
docker compose -f docker-compose.dev.yml up -d db
@@ -217,6 +217,15 @@ SECRET_KEY=<SECRET_KEY>
ANTHROPIC_API_KEY=<ANTHROPIC_API_KEY>
GOOGLE_AI_API_KEY=<GOOGLE_AI_API_KEY or leave unset>
POSTGRES_PORT=<POSTGRES_PORT>
# Absolute host-side path to the repo root. REQUIRED whenever docker-compose is
# invoked from inside a container (e.g. a code-server container with the host
# Docker socket mounted in). Without it, the bind mounts in
# docker-compose.dev.yml (`${REPO_ROOT}/backend:/app`, `${REPO_ROOT}/frontend:/app`)
# resolve against the CLI's CWD — a path the host daemon cannot see — and
# Docker silently creates empty directories there instead of mounting the code.
# If you run docker compose directly on the host shell, you can set this to `.`
# or the absolute path of the repo; being explicit is safer either way.
REPO_ROOT=/absolute/path/to/resolutionflow
```
> **Never commit any `.env` file.** The `.gitignore` already covers this.
@@ -487,6 +496,14 @@ Only on a local branch that has diverged from `origin/main`. Production `main` h
Filesystem permission issue inside the code-server container (CLAUDE.md Lesson 105). TypeScript compilation itself completes — use `npx tsc -b` to verify cleanliness without needing to write to `dist/`.
### Backend/frontend containers start but `/app` is empty (no code mounted)
Almost always a `REPO_ROOT` problem. `docker-compose.dev.yml` uses `${REPO_ROOT}/backend:/app` and `${REPO_ROOT}/frontend:/app` bind mounts. If `REPO_ROOT` is unset, or set to a path that doesn't exist *on the Docker host* (not inside the code-server container), Docker silently creates an empty directory at that path and mounts it — the containers come up but have no source code. Symptom: backend returns import errors, or frontend serves a default Vite page. Fix: set `REPO_ROOT` in the repo-root `.env` to the absolute host-side path to the repo, then `docker compose down && docker compose up -d`. See 5.4 for the full note. This matters specifically when `docker compose` is invoked from inside a container (e.g. code-server with the host Docker socket mounted) — the CLI's CWD is container-local but the daemon resolves paths against the host filesystem.
### Frontend shows "Blocked request. This host is not allowed" in the browser
Vite 5+ ships DNS-rebinding protection that rejects any `Host:` header not in `server.allowedHosts`. The browser's hostname must be in that list. Edit `frontend/vite.config.ts` — the `server.allowedHosts` array should include every hostname you reach the dev server from (e.g. `'docker-01'`, `'localhost'`, `.ts.net` as a wildcard for Tailscale MagicDNS). Restart the Vite dev server (for Option B: `docker compose restart frontend`). This is unrelated to CORS — Vite blocks the request before any app code runs.
### `docker` command not found inside code-server
If your code-server is itself inside a container, Docker is probably not exposed to it. CLAUDE.md Lesson 103 was written for this case on the old VPS. On Proxmox, the fix depends on topology — either SSH to the host to run Docker commands, or mount the host's Docker socket into the code-server container.
@@ -516,8 +533,139 @@ This doc is about dev, not production. But:
## 10. What's not in this doc
- **Production deployment.** This is a dev-env doc. Production lives on Railway — see `CLAUDE.md`'s Deployment section.
- **How to set up Traefik or any particular reverse proxy.** Whichever proxy you use is your choice; the dev stack just needs something that routes `<host>:5173` and `<host>:8000` to the right services.
- **How to set up Traefik or any particular reverse proxy.** Whichever proxy you use is your choice; the dev stack just needs something that routes `<host>:5173` and `<host>:8000` to the right services. **Direct port exposure over a private network** (Tailscale, WireGuard, a VPN, or a LAN behind a firewall) is a fully supported option for dev and is what the homelab reference topology in Section 11 uses — no reverse proxy, no TLS, just `http://<host>:5173` and `http://<host>:8000` reachable only from the private network. That's a perfectly reasonable choice; it's just not the only one.
- **How to configure code-server itself.** Install it however you prefer (native, Docker, LXC); point it at the repo, and the rest of this doc applies.
- **Where to host the Proxmox instance.** Up to you.
If something in this doc turns out to be wrong on your host, fix the doc. This is a living document — the whole point of rewriting it from the Hostinger-specific version was to make it survive host changes.
---
## 11. Reference topology: homelab Proxmox + code-server (Option B)
This section documents the first concrete host instantiation since the April 2026 host-agnostic rewrite. It's a worked example, not the canonical topology — Section 3's Option A/B/C framing still stands. If your setup looks different, follow Sections 110 and ignore this appendix.
### 11.1 Host
- **Hypervisor:** Proxmox (homelab).
- **VM:** `docker-01`, Debian 13, running Docker Engine + Docker Compose natively.
- **Tailscale IP:** `100.64.78.44`. MagicDNS hostname: `docker-01` (and the full `.ts.net` FQDN).
- **code-server:** runs on the same VM in its own container, with the host's Docker socket mounted in so it can drive `docker compose`. Its workspace bind-mounts the repo at `/opt/docker/code-server/workspace/resolutionflow`.
This is a concrete instance of Option B from Section 3: Postgres, backend, and frontend all run as containers from `docker-compose.dev.yml`; the editor lives outside that compose network.
### 11.2 Access pattern — direct port over Tailscale, no reverse proxy
The browser reaches the dev stack directly:
- Frontend: `http://docker-01:5173`
- Backend: `http://docker-01:8000`
- Backend API docs: `http://docker-01:8000/api/docs`
There is **no Caddy, no Traefik, no nginx, no TLS, no basic auth** in front of either service. The tailnet provides the wire encryption and access control — only devices on the tailnet can resolve `docker-01` or reach `100.64.78.44`, and Tailscale ACLs decide which of those devices are allowed to connect.
Why this choice:
- **Zero routing config to maintain.** There is no proxy rulebook to keep in sync with new services. Add a container, expose a port, you're done.
- **Backend-to-backend services stay private.** Redis, Celery workers, the planned ConnectWise proxy, the MCP server — none of them need to be reachable from the browser, so none of them need proxy rules. They stay inside the `resolutionflow` Docker network and talk by service name. The proxy would only ever have carried frontend and backend traffic, so the proxy's value was small relative to its maintenance cost.
- **Debuggability.** `curl http://docker-01:8000/api/docs` from any tailnet device works without auth headers, TLS handshakes, or DNS shenanigans.
Tradeoff: **this only works because every client device is on the tailnet.** If someone needed to test from a non-tailnet device, they'd either join the tailnet or we'd need to front the stack with a proxy. For the current single-developer setup, the tailnet-only assumption holds.
### 11.3 Per-host config values (as actually configured on `docker-01`)
Plugging these into Section 4's template:
```
DEV_HOST = docker-01
DEV_HOST_SCHEME = http
FRONTEND_PORT = 5173
BACKEND_PORT = 8000
POSTGRES_PORT = 5433 # host-side; container-internal stays 5432
POSTGRES_DB_NAME = resolutionflow
POSTGRES_USER = postgres
POSTGRES_PASSWORD = postgres # local-dev only
SECRET_KEY = <generated per host; do not reuse>
ANTHROPIC_API_KEY = <from console.anthropic.com>
GOOGLE_AI_API_KEY = <unset; Anthropic is sole provider in dev>
```
And the repo-root `.env` that `docker-compose.dev.yml` interpolates from:
```bash
SECRET_KEY=<redacted>
ANTHROPIC_API_KEY=<redacted>
POSTGRES_PORT=5433
REPO_ROOT=/opt/docker/code-server/workspace/resolutionflow
```
### 11.4 Why `REPO_ROOT` is non-optional on this host
code-server runs inside a container. When you open a terminal in code-server and run `docker compose -f docker-compose.dev.yml up -d`, the Docker CLI talks to the *host* daemon via the mounted socket — but the CWD it reports (`/config/workspace/resolutionflow`) is a path that only exists inside the code-server container. The host daemon has never heard of it.
Relative bind mounts like `./backend:/app` therefore resolve against a path the host can't see, and Docker silently creates empty directories there rather than erroring out. The containers come up, but `/app` is empty.
`docker-compose.dev.yml` sidesteps this by using `${REPO_ROOT}/backend:/app` and `${REPO_ROOT}/frontend:/app`. `REPO_ROOT` must be set to the absolute path **on the host** (`/opt/docker/code-server/workspace/resolutionflow`), not the path inside the code-server container. Same contents, different mount point, different name.
If you ever run `docker compose` directly from a host shell (SSH'd into `docker-01`), set `REPO_ROOT` to `.` or the absolute host path. Being explicit is always safe; leaving it unset is the failure mode.
### 11.5 Vite `server.allowedHosts` — required for `docker-01` to resolve
Vite 5+ rejects any `Host:` header not in `server.allowedHosts` (DNS-rebinding protection). `frontend/vite.config.ts` has:
```ts
server: {
host: '0.0.0.0',
allowedHosts: ['docker-01', '.ts.net', 'localhost'],
...
}
```
- `docker-01` — the MagicDNS short name the browser uses day-to-day.
- `.ts.net` — wildcard for the full Tailscale MagicDNS FQDN, in case anyone uses it.
- `localhost` — for the "am I serving anything at all" smoke-test from inside the container.
If you move this setup to a different host, add that host's hostname to `allowedHosts` or the browser will see "Blocked request. This host is not allowed." See Section 8's troubleshooting entry for the full symptom/fix.
### 11.6 CORS origins on this host
The `backend` service's `CORS_ORIGINS` environment variable is pinned in the compose file to:
```
["http://localhost:5173","http://127.0.0.1:5173","http://docker-01:5173","http://100.64.78.44:5173"]
```
The last two are what make browser calls from tailnet clients work — they cover both MagicDNS (`docker-01`) and the raw Tailscale IP. If you add a new hostname to reach the frontend from, also add the matching origin here and restart the backend.
### 11.7 Compose file shape (as of this writing)
`docker-compose.dev.yml` has been through a round of cleanup for this topology. Specifics worth knowing if you're comparing against older revisions of the file:
- **No Traefik labels.** They were removed — nothing in this topology uses Traefik.
- **No Hostinger-VPS-era origins** in `CORS_ORIGINS`.
- `Dockerfile.dev` for both `backend` and `frontend` is still the build source — this didn't change.
- Explicit `command:` directives on both `backend` (`uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload`) and `frontend` (`npm run dev -- --host 0.0.0.0 --port 5173`) — this guarantees `--host 0.0.0.0` regardless of what's baked into the image, so the services listen on all interfaces and are reachable from outside the container.
- `REPO_ROOT` is interpolated into both service volume mounts (see 11.4).
If you're adapting the file for a different host, the things most likely to need editing are `REPO_ROOT` (see 11.4), `CORS_ORIGINS` (see 11.6), `FRONTEND_URL`, `VITE_API_URL`, and `POSTGRES_PORT` if you want something other than `5433`.
### 11.8 End-to-end sanity check for this topology
From any device on the tailnet:
```bash
# Backend reachable
curl -sSf http://docker-01:8000/api/docs >/dev/null && echo OK
# Frontend reachable
curl -sSf http://docker-01:5173 >/dev/null && echo OK
# Alembic head matches the branch expectation
docker exec resolutionflow_backend alembic current
# expect f07010f17b01 on feat/flowpilot-migration, 074 on main
# Postgres is alive inside the compose network
docker exec resolutionflow_postgres psql -U postgres -d resolutionflow -c "SELECT now();"
```
All four passing = the dev environment is live end-to-end.

View File

@@ -1,4 +1,5 @@
name: resolutionflow
services:
db:
image: pgvector/pgvector:pg16
@@ -8,7 +9,7 @@ services:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: resolutionflow
ports:
- "${POSTGRES_PORT:-5432}:5432"
- "${POSTGRES_PORT:-5433}:5432"
volumes:
- rf_postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -22,8 +23,11 @@ services:
context: ./backend
dockerfile: Dockerfile.dev
container_name: resolutionflow_backend
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
ports:
- "8000:8000"
volumes:
- ./backend:/app
- ${REPO_ROOT}/backend:/app
environment:
- APP_NAME=ResolutionFlow
- DEBUG=true
@@ -33,42 +37,34 @@ services:
- ALGORITHM=HS256
- ACCESS_TOKEN_EXPIRE_MINUTES=15
- REFRESH_TOKEN_EXPIRE_DAYS=7
- REQUIRE_INVITE_CODE=true
- FEEDBACK_EMAIL=feedback@resolutionflow.com
- AI_PROVIDER=anthropic
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- GOOGLE_AI_API_KEY=${GOOGLE_AI_API_KEY}
- CORS_ORIGINS=["http://localhost:3000","http://localhost:5173","http://127.0.0.1:3000","http://127.0.0.1:5173","http://46.202.92.250:5173","http://46.202.92.250:3000","https://resolutionflow.com","https://www.resolutionflow.com"]
- GOOGLE_AI_API_KEY=${GOOGLE_AI_API_KEY:-}
- ENABLE_MCP_MICROSOFT_LEARN=true
- FRONTEND_URL=http://docker-01:5173
- CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173","http://docker-01:5173","http://100.64.78.44:5173"]
depends_on:
db:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.rf-api.rule=Host(`dev.resolutionflow.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.rf-api.entrypoints=websecure"
- "traefik.http.routers.rf-api.tls.certresolver=letsencrypt"
- "traefik.http.services.rf-api.loadbalancer.server.port=8000"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
container_name: resolutionflow_frontend
command: npm run dev -- --host 0.0.0.0 --port 5173
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- ${REPO_ROOT}/frontend:/app
- /app/node_modules
environment:
- VITE_API_URL=https://dev.resolutionflow.com/
- VITE_API_URL=http://docker-01:8000
- CHOKIDAR_USEPOLLING=true
depends_on:
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.rf-frontend.rule=Host(`dev.resolutionflow.com`)"
- "traefik.http.routers.rf-frontend.middlewares=dev-auth"
- "traefik.http.middlewares.dev-auth.basicauth.users=chihlasm:$$apr1$$dJXUAZ3Y$$SsJm.K8fOjCeNMe4B70Bi0"
- "traefik.http.routers.rf-frontend.entrypoints=websecure"
- "traefik.http.routers.rf-frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.rf-frontend.loadbalancer.server.port=5173"
volumes:
rf_postgres_data:
rf_postgres_data:

View File

@@ -1,120 +0,0 @@
# FlowPilot Migration — Handoff Note
> **Purpose:** Self-contained status snapshot for picking up Phase 0 + Phase 1 work after the Proxmox dev-environment migration.
> **Lifespan:** Ephemeral. Delete this file once Phase 0 + Phase 1 verification has passed on the new environment.
> **Branch:** `feat/flowpilot-migration` (pushed to Gitea; mirrors to GitHub).
> **Last commit as of this writing:** `f3c3ee5 feat(pilot): unify AI troubleshooting surface at /pilot, redirect /assistant (Phase 1)`
---
## 1. Read order for a fresh Claude Code session
1. `CLAUDE.md` at repo root — Lessons 41+ are current project context; lessons written for the old VPS (especially 66 and 103) may be outdated on Proxmox.
2. `DEV-ENV.md` — post-migration rewrite. If you are setting up the dev environment on Proxmox for the first time, start there.
3. `docs/FlowAssist_Migration/FLOWPILOT-MIGRATION.md` — the locked design doc. Section 0.1 lists the spec drift already integrated into this revision. Section 9 lists all phases and their verification steps.
4. This file — the handoff-state snapshot. Tells you what is pushed, what is verified, and what is owed.
5. `git log --oneline origin/main..HEAD` — should show ten commits ending at `f3c3ee5`. If it shows something different, ground yourself before acting.
---
## 2. What is done, committed, and pushed
Ten commits on `feat/flowpilot-migration`:
| Commit | Phase | Summary |
|---|---|---|
| `46291f3` | Setup | Migration design doc + mockups brought onto the branch |
| `0fbc1e0` | 0.5 | MCP per-turn structured-log telemetry (`mcp.turn`, `mcp.fallback`) |
| `b3be666` | 0.1 | Structured-system-block caching in `AnthropicProvider` (policy α) |
| `56fd440` | 0.2 | Doc-drift note — `/tickets/ai-parse` endpoint does not exist; target pending |
| `da93ae5` | 0.3 | Opt-in caching for `ai_tree_generator`, `kb_conversion`, `ai_fix`, `script_builder` |
| `3f0a132` | 0.4 | `_call_anthropic_cached``chat_call_cached`; cache plumbing extracted |
| `92fadfb` | Docs | Codex plan review + Phase 0 audit findings integrated into migration doc |
| `210d310` | 1 | Alembic migration `f07010f17b01` — new tables + column adds + RLS |
| `b49772f` | 1 | SQLAlchemy models: `SessionFact`, `SessionSuggestedFix`, `DraftTemplate`, `AccountSettings` |
| `f3c3ee5` | 1 | `/pilot` = unified surface; `/assistant` redirects; `FlowPilotSessionPage` unmounted |
All ten are on `origin/feat/flowpilot-migration`.
---
## 3. What is NOT done (verification owed on the new env)
Every item below was deferred because the old dev environment was offline during the work.
### Gate 1 — runtime verification
1. **Alembic upgrade.** `cd backend && alembic upgrade head` — must end on revision `f07010f17b01`. Fresh database, no other branches of the history polluting it.
2. **Alembic downgrade reversibility.** From `f07010f17b01`, run `alembic downgrade -1`, then `alembic upgrade head` again. Must succeed both ways.
3. **Cache-hit verification (Phase 0 TODO).** The `TODO(phase0-verify)` note in `backend/app/core/ai_provider.py` module docstring describes the procedure:
- Start the backend with `AI_PROVIDER=anthropic` and `ANTHROPIC_API_KEY` set.
- Hit any FlowPilot endpoint twice within 5 minutes (the ephemeral-cache TTL).
- Grep backend logs for `anthropic.cache` log events.
- First turn should show `cache_creation_input_tokens > 0`.
- Second turn should show `cache_read_input_tokens > 0`.
- If the second turn returns zero reads, inspect the prefix for silent invalidators (timestamps, unsorted JSON keys, varying tool list ordering). Fix before proceeding.
4. **Frontend build.** `cd frontend && npm run build` — must complete. TypeScript `noUnusedLocals` hard-errors on orphan imports (CLAUDE.md Lesson 92). Particular risk: `FlowPilotSessionPage.tsx` is unmounted but on disk — I removed the router import, but a missed reference elsewhere in the tree would surface as a build error.
5. **Redirect smoke test.** Open `http://<new-host>:5173/assistant/<any-real-session-id>` in a browser. Must land on `/pilot/<that-id>` with the ID preserved.
6. **Dispatcher smoke test.** On the dashboard, click a session in `ActiveFlowPilotSessions` or `RecentFlowPilotSessions`. Must route to `/pilot/:id` regardless of the session's `session_type` value.
If any of these fail, they are debug tasks on the new env. They are not reasons to revert Phase 0 or Phase 1.
### Gate 2 — Phase 2 prerequisites (not blocking verification, blocking Phase 2 start)
- Task-lane item stable UUID assignment. Section 4.2 + Phase 2 Deliverable 1 of the design doc. Must land before any `session_facts.source_ref` can point at anything reliable.
- GitNexus re-index. Run `npx gitnexus analyze` before Phase 2 starts — Phase 2 touches `unified_chat_service` and the marker parser, which are load-bearing across the app. I deferred the re-index through all of Phase 0 and Phase 1 because both were file-scoped.
---
## 4. Known drift and intentional deferrals
### Patherly / Resolutionflow naming
49 references to `patherly` remain across the repo (most in historical docs). Live-code hits you may touch during setup:
- `backend/alembic.ini`
- `backend/app/core/config.py`
- `backend/docker-compose.yml`
- `backend/tests/conftest.py`
- `backend/tests/test_rls_isolation.py`
- `frontend/src/lib/sessionRatings.ts` (localStorage key — renaming drops user rating state; flag but do not change)
- `README.md`
- `SESSION-HANDOFF.md`
- `CLAUDE.md`
The canonical name going forward is `resolutionflow`. Sweep is owner-driven in a dedicated commit — do not silently rename as a side effect of setup work.
### FlowPilotSessionPage.tsx still on disk
The file is retained but unreachable. Delete it in a follow-up commit once nothing else in the tree imports it. The comment in `frontend/src/router.tsx` around the old import location explains the disposition.
### Sidebar UX decision deferred
Whether FlowPilot deserves its own sidebar rail entry (currently it's only under the "History" group's `matchPaths`) is a product decision, not Phase 1 scope.
### Pre-existing local alembic divergence
During Phase 1 I surfaced that the local branch showed five alembic heads, but production `main` has a single head at `074` — the extra local heads were branch artifacts. My Phase 1 migration `f07010f17b01` chains from `074`. If `alembic heads` on a fresh DB after `upgrade head` shows anything other than one head ending at `f07010f17b01`, stop and investigate.
---
## 5. Recommended order of operations after the move
1. Clone the repo on the new host. Check out `feat/flowpilot-migration`. Confirm `git log --oneline origin/main..HEAD` shows the ten commits above.
2. Follow `DEV-ENV.md` to stand up the dev environment on Proxmox.
3. Work Gate 1 (items 1 through 6 above) in order. Stop at the first failure.
4. If Gate 1 passes, decide whether to PR `feat/flowpilot-migration` into `main` or keep it open pending Phase 2 work.
5. Before starting Phase 2, complete Gate 2 (task-lane UUIDs + GitNexus re-index).
6. Delete this file (`docs/FlowAssist_Migration/MIGRATION-HANDOFF.md`) once Gate 1 is fully passed. Commit message: `chore(flowpilot-migration): remove migration handoff note after verification`.
---
## 6. Contact surface for context
This migration was authored by Claude Code in a session with Michael. If you are a different Claude Code session or developer:
- The full decision log lives in `docs/FlowAssist_Migration/FLOWPILOT-MIGRATION.md` Section 14.
- The pre-rewrite version is at `docs/FlowAssist_Migration/FLOWPILOT-MIGRATION-v1.md`.
- The Codex plan review is at `docs/FlowAssist_Migration/CODEX-FlowAssist-Migration-PLAN.md`.
If any of those three files are missing or out of date relative to the branch state described here, flag it rather than guess.

View File

@@ -19,7 +19,7 @@ export default defineConfig({
],
server: {
host: '0.0.0.0',
allowedHosts: ['dev.resolutionflow.com'],
allowedHosts: ['docker-01', '.ts.net', 'localhost'],
watch: {
usePolling: true,
},