diff --git a/CLAUDE-SETUP.md b/CLAUDE-SETUP.md index 64d3b906..16c4e752 100644 --- a/CLAUDE-SETUP.md +++ b/CLAUDE-SETUP.md @@ -381,11 +381,14 @@ curl -X GET "http://localhost:8000/api/v1/trees" -H "Authorization: Bearer **Purpose:** Quick-reference file showing exactly where the project stands. +> **For Claude Code:** Read this first to understand what's done and what's next. +> **Last Updated:** January 28, 2026 + +--- + +## Active Phase: Phase 2 - Tree Editor (In Progress) + +--- + +## What's Complete โœ… + +### Backend (100%) +- โœ… FastAPI project structure +- โœ… PostgreSQL database with Docker +- โœ… User authentication (JWT, register, login, refresh) +- โœ… Trees CRUD with full-text search +- โœ… Sessions tracking with decisions +- โœ… Export API (Markdown, Text, HTML) +- โœ… Role-based access control foundation +- โœ… Production-ready logging with correlation IDs +- โœ… 40+ integration tests +- โœ… DateTime timezone handling fixed + +### Frontend (In Progress) +- โœ… React + Vite + TypeScript + Tailwind setup +- โœ… Authentication UI (login, register) +- โœ… Basic layout and navigation +- โœ… Tree library/browsing page +- โœ… Tree navigation interface +- โœ… Session management +- โœ… Export functionality (download) +- โœ… Responsive design +- โœ… Error boundaries +- โœ… **Tree Editor** - Form-based with visual preview + - โœ… Zustand store with immer (undo/redo via zundo) + - โœ… Split-view layout (editor left, preview right) + - โœ… Node CRUD (Decision, Action, Solution types) + - โœ… NodePicker with type-grouped dropdown + - โœ… Dynamic array fields (options, commands, steps) + - โœ… Visual tree preview with solution indicators + - โœ… Shared node detection (multiple sources โ†’ same target) + - โœ… Modal with scrollable content, fixed header/footer +- โณ User preferences (dark mode) - NOT YET STARTED +- โณ Keyboard shortcuts - NOT YET STARTED + +### Documentation +- โœ… Project overview and architecture docs +- โœ… Development roadmap through Phase 4 +- โœ… Feature specifications (including Phase 2.5) +- โœ… CLAUDE-SETUP.md for onboarding +- โœ… LESSONS-LEARNED.md for avoiding past mistakes + +--- + +## What's In Progress ๐Ÿ”„ + +| Task | Status | Notes | +|------|--------|-------| +| Tree Editor | Functional | Core editing complete, polish ongoing | +| Tree Editor Validation | Partial | Basic validation working | +| User Preferences | Not started | Dark/light mode, export format default | +| TypeScript strict mode | Warnings exist | tsconfig needs `strict: true` | +| Starter decision trees | 1 of 5 complete | Need 4 more real trees | +| Deployment | Not started | Railway/Render planned | + +--- + +## What's Next (Priority Order) + +### Immediate (This Week) +1. Complete Tree Editor validation (required fields, orphan detection) +2. Add User Preferences (theme toggle, export format default) +3. Fix TypeScript strict mode warnings +4. Create remaining 4 starter decision trees + +### Soon (Phase 2 Completion) +- Team management +- Mobile responsive improvements +- Tree versioning UI + +### Later (Phase 2.5) +- Personal tree branching +- Step library with ratings +- Tree forking and sharing + +--- + +## Key Files Reference + +### Backend +``` +backend/ +โ”œโ”€โ”€ app/ +โ”‚ โ”œโ”€โ”€ main.py # FastAPI entry point +โ”‚ โ”œโ”€โ”€ api/v1/endpoints/ # API route handlers +โ”‚ โ”‚ โ”œโ”€โ”€ auth.py +โ”‚ โ”‚ โ”œโ”€โ”€ trees.py +โ”‚ โ”‚ โ””โ”€โ”€ sessions.py +โ”‚ โ”œโ”€โ”€ models/ # SQLAlchemy models +โ”‚ โ”œโ”€โ”€ schemas/ # Pydantic schemas +โ”‚ โ””โ”€โ”€ core/ +โ”‚ โ”œโ”€โ”€ config.py # Settings +โ”‚ โ”œโ”€โ”€ security.py # JWT handling +โ”‚ โ””โ”€โ”€ logging_config.py +โ”œโ”€โ”€ alembic/ # Database migrations +โ”œโ”€โ”€ tests/ # pytest tests +โ””โ”€โ”€ requirements.txt +``` + +### Frontend +``` +frontend/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ main.tsx # Entry point +โ”‚ โ”œโ”€โ”€ App.tsx # Router setup +โ”‚ โ”œโ”€โ”€ pages/ # Page components +โ”‚ โ”‚ โ””โ”€โ”€ TreeEditorPage.tsx +โ”‚ โ”œโ”€โ”€ components/ +โ”‚ โ”‚ โ”œโ”€โ”€ common/ # Modal, etc. +โ”‚ โ”‚ โ”œโ”€โ”€ tree-editor/ # Tree Editor components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TreeEditorLayout.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TreeMetadataForm.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NodeList.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NodeEditorModal.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NodeFormDecision.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NodeFormAction.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NodeFormResolution.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DynamicArrayField.tsx +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ NodePicker.tsx +โ”‚ โ”‚ โ””โ”€โ”€ tree-preview/ # Visual preview +โ”‚ โ”‚ โ”œโ”€โ”€ TreePreviewPanel.tsx +โ”‚ โ”‚ โ””โ”€โ”€ TreePreviewNode.tsx +โ”‚ โ”œโ”€โ”€ store/ +โ”‚ โ”‚ โ”œโ”€โ”€ authStore.ts +โ”‚ โ”‚ โ””โ”€โ”€ treeEditorStore.ts # Zustand + immer + zundo +โ”‚ โ”œโ”€โ”€ contexts/ # React contexts (auth) +โ”‚ โ”œโ”€โ”€ hooks/ # Custom hooks +โ”‚ โ””โ”€โ”€ api/ # API client +โ”œโ”€โ”€ tailwind.config.js +โ””โ”€โ”€ tsconfig.json +``` + +### Documentation +``` +Apoklisis/ +โ”œโ”€โ”€ CLAUDE-SETUP.md # Full context for Claude Code +โ”œโ”€โ”€ CURRENT-STATE.md # This file - quick status +โ”œโ”€โ”€ LESSONS-LEARNED.md # Bugs and fixes reference +โ”œโ”€โ”€ 01-PROJECT-OVERVIEW.md +โ”œโ”€โ”€ 02-TECHNICAL-ARCHITECTURE.md +โ”œโ”€โ”€ 03-DEVELOPMENT-ROADMAP.md +โ”œโ”€โ”€ 04-FEATURE-SPECIFICATIONS.md +โ””โ”€โ”€ PHASE-2.5-PERSONAL-BRANCHING.md # Detailed Phase 2.5 spec +``` + +--- + +## Environment Quick Reference + +### Start Development +```powershell +# Terminal 1: Database +docker start apoklisis_postgres + +# Terminal 2: Backend +cd C:\Dev\Projects\Apoklisis\backend +.\venv\Scripts\activate +uvicorn app.main:app --reload + +# Terminal 3: Frontend +cd C:\Dev\Projects\Apoklisis\frontend +npm run dev +``` + +### URLs +- Frontend: http://localhost:5173 +- Backend API: http://localhost:8000 +- API Docs: http://localhost:8000/docs + +### Run Tests +```powershell +cd C:\Dev\Projects\Apoklisis\backend +.\venv\Scripts\activate +pytest +``` + +--- + +## Recent Changes (Jan 28, 2026) + +1. Fixed DateTime timezone bugs in all models +2. Added production logging system +3. Created 40+ integration tests +4. Added Phase 2.5 specifications (Personal Branching, Step Library) +5. Added User Preferences to MVP scope +6. Created LESSONS-LEARNED.md +7. Created CURRENT-STATE.md (this file) +8. **Tree Editor Implementation**: + - Zustand store with immer middleware and zundo for undo/redo + - Form-based node editing with type-specific forms + - NodePicker dropdown grouped by node type (Decision/Action/Solution) + - Visual tree preview with recursive rendering + - Solution connection indicators (green checkmark badges) + - Shared node detection showing when multiple nodes link to same target + - Modal component with scrollable body, fixed header/footer + +--- + +## Blockers / Known Issues + +| Issue | Workaround | Status | +|-------|------------|--------| +| pytest-asyncio version conflict | Use 0.24.0 | Documented | +| No local psql on Windows | Use `docker exec` | Documented | + +--- + +## Session Handoff Notes + +*Update this section at the end of each coding session:* + +**Last Session (Jan 28, 2026):** +- Completed Tree Editor core implementation +- Fixed modal scroll/overflow issue (content scrolls, header/footer fixed) +- Added SharedLinksMap for tracking nodes that link to same target +- Improved NodePicker with type-grouped dropdown +- Added solution connection indicators in preview +- Next: Tree Editor validation polish, user preferences UI diff --git a/LESSONS-LEARNED.md b/LESSONS-LEARNED.md new file mode 100644 index 00000000..77696888 --- /dev/null +++ b/LESSONS-LEARNED.md @@ -0,0 +1,437 @@ +# Lessons Learned + +> **Purpose:** This file documents bugs, fixes, and gotchas encountered during development. +> **For Claude Code:** Read this file at the start of each session to avoid repeating past mistakes. +> **Last Updated:** January 28, 2026 + +--- + +## Python / Backend + +### DateTime Timezone Handling โš ๏ธ CRITICAL +**Problem:** SQLAlchemy `DateTime` fields caused Internal Server Errors when mixing timezone-aware and timezone-naive datetimes. + +**Error:** `can't subtract offset-naive and offset-aware datetimes` + +**Solution:** +- Always use `DateTime(timezone=True)` in SQLAlchemy models +- Always use `datetime.now(timezone.utc)` for defaults and assignments +- Never use `datetime.utcnow()` (deprecated and timezone-naive) + +**Correct pattern:** +```python +from datetime import datetime, timezone +from sqlalchemy import DateTime + +created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) +``` + +**Files affected:** All models with timestamp fields (user.py, tree.py, session.py, team.py, attachment.py) + +--- + +### pytest-asyncio Version Compatibility +**Problem:** Tests fail with `AttributeError: 'Package' object has no attribute 'obj'` + +**Cause:** Incompatibility between pytest 8.0.0 and pytest-asyncio 0.23.3 + +**Solution:** Upgrade pytest-asyncio to 0.24.0 +```powershell +pip install pytest-asyncio==0.24.0 --upgrade +``` + +--- + +### bcrypt / passlib Compatibility +**Problem:** Password hashing fails with newer bcrypt versions. + +**Solution:** Pin bcrypt version in requirements.txt: +``` +bcrypt==4.0.1 +passlib[bcrypt]==1.7.4 +``` + +--- + +### Virtual Environment Best Practices +**Problem:** OneDrive sync causes conflicts with venv, `__pycache__`, and lock files. + +**Solution:** +- Keep project in `C:\Dev\Projects\`, NOT in OneDrive-synced folders +- Add to `.gitignore`: `venv/`, `__pycache__/`, `*.pyc` + +--- + +### Installing Packages in venv +**Problem:** `pip install` sometimes installs globally instead of in venv. + +**Solution:** +- Always verify venv is active: look for `(venv)` prefix in terminal +- If VS Code prompts to create a new venv when one exists, click "Don't show again" +- Use `pip install --break-system-packages` flag if needed (Python 3.12+) + +--- + +### Alembic Migrations +**Problem:** Model changes not reflected in database. + +**Solution:** Always run migrations after model changes: +```powershell +cd backend +alembic revision --autogenerate -m "Description of change" +alembic upgrade head +``` + +--- + +## TypeScript / Frontend + +### React State: Don't Store Object Snapshots for Editing โš ๏ธ CRITICAL +**Problem:** Form inputs don't update when typing - characters appear then immediately disappear. + +**Cause:** Storing a full object from a Zustand store in local state creates a snapshot. When the store updates, the local state still holds the old reference, causing the input's `value` to revert. + +**Broken pattern:** +```tsx +// BAD: Stores a snapshot that won't update +const [editingNode, setEditingNode] = useState(null) + +// When modal opens: +setEditingNode(node) // snapshot captured here + +// In modal form: + // always shows old value +``` + +**Solution:** Store only the ID, then fetch the current object from the store on each render: +```tsx +// GOOD: Store only the ID +const [editingNodeId, setEditingNodeId] = useState(null) +const editingNode = editingNodeId ? findNode(editingNodeId) : null + +// When modal opens: +setEditingNodeId(node.id) + +// In modal form - now gets fresh data each render: + +``` + +**Why it works:** By calling `findNode()` on each render, the component always gets the current state from the store after updates. + +**Files affected:** Any component that opens a modal/form to edit store data (NodeList.tsx) + +--- + +### Modal Scroll/Overflow with Fixed Header and Footer +**Problem:** Modal content extends beyond screen when there's too much content, pushing close button and action buttons off-screen. + +**Solution:** Use flex layout with fixed header/footer and scrollable body: +```tsx +// Modal structure +
+ {/* Header - fixed at top */} +
+

{title}

+ +
+ + {/* Body - scrollable */} +
+ {children} +
+ + {/* Footer - fixed at bottom */} + {footer && ( +
+ {footer} +
+ )} +
+``` + +**Key points:** +- `max-h-[85vh]` constrains total modal height +- `flex-col` enables vertical flex layout +- `flex-shrink-0` on header/footer prevents them from shrinking +- `flex-1 overflow-y-auto` on body makes it fill remaining space and scroll + +**Files affected:** Modal.tsx, any component using modals for forms + +--- + +### Lucide React Icons: No Title Prop +**Problem:** TypeScript error when trying to add `title` prop to Lucide icons. + +**Error:** `Property 'title' does not exist on type 'LucideProps'` + +**Broken pattern:** +```tsx + // โŒ Error +``` + +**Solution:** Wrap the icon in a span with the title: +```tsx + + + +``` + +--- + +### Tree Traversal: Preventing Infinite Loops with Visited Set +**Problem:** When traversing a tree structure that has cross-references (like `next_node_id` pointing to nodes elsewhere in the tree), you can get infinite loops. + +**Solution:** Use a `visited` Set to track already-processed nodes: +```tsx +function hasSolutionInSubtree( + node: TreeStructure, + findNode: (id: string) => TreeStructure | null, + visited: Set = new Set() +): boolean { + // Prevent infinite loops + if (visited.has(node.id)) return false + visited.add(node.id) + + if (node.type === 'solution') return true + + // Check children array + if (node.children) { + for (const child of node.children) { + if (hasSolutionInSubtree(child, findNode, visited)) return true + } + } + + // Check next_node_id reference (could point anywhere in tree) + if (node.next_node_id) { + const nextNode = findNode(node.next_node_id) + if (nextNode && hasSolutionInSubtree(nextNode, findNode, visited)) { + return true + } + } + + return false +} +``` + +**Why it matters:** Decision trees can have shared nodes where multiple paths converge on the same target. Without loop detection, recursive traversal will hang. + +--- + +### SharedLinksMap Pattern for Tracking Cross-References +**Problem:** Need to know which nodes link to the same target node (for showing "shared by X nodes" indicators). + +**Solution:** Build a map at the parent component level, pass down to children: +```tsx +// Type definition +type SharedLinksMap = Map> + +// Build the map by traversing tree once +function buildSharedLinksMap( + node: TreeStructure, + map: SharedLinksMap = new Map() +): SharedLinksMap { + const nodeLabel = node.type === 'decision' ? node.question : node.title + + // Record decision option targets + if (node.type === 'decision' && node.options) { + for (const opt of node.options) { + if (opt.next_node_id) { + const existing = map.get(opt.next_node_id) || [] + existing.push({ id: node.id, label: nodeLabel || 'Untitled' }) + map.set(opt.next_node_id, existing) + } + } + } + + // Record action next_node_id targets + if (node.type === 'action' && node.next_node_id) { + const existing = map.get(node.next_node_id) || [] + existing.push({ id: node.id, label: nodeLabel || 'Untitled' }) + map.set(node.next_node_id, existing) + } + + // Recurse + if (node.children) { + for (const child of node.children) { + buildSharedLinksMap(child, map) + } + } + + return map +} + +// Use in parent with useMemo +const sharedLinksMap = useMemo(() => { + if (!treeStructure) return new Map() + return buildSharedLinksMap(treeStructure) +}, [treeStructure]) +``` + +**Usage in child:** Check `sharedLinksMap.get(node.id)?.length > 1` to see if node is shared. + +--- + +### tsconfig.json Strict Mode +**Problem:** VS Code shows warnings about missing compiler options. + +**Solution:** Add to `compilerOptions` in tsconfig.json: +```json +{ + "compilerOptions": { + "strict": true, + "forceConsistentCasingInFileNames": true + } +} +``` + +**Why it matters:** `forceConsistentCasingInFileNames` prevents issues when deploying from Windows (case-insensitive) to Linux (case-sensitive). + +--- + +### Tailwind Dark Mode +**Pattern:** Use Tailwind's `dark:` variant for dark mode styling. + +**Setup:** In `tailwind.config.js`: +```javascript +module.exports = { + darkMode: 'class', // or 'media' for system preference only + // ... +} +``` + +**Usage:** +```jsx +
+``` + +--- + +## Docker / PostgreSQL + +### Accessing PostgreSQL Without Local psql +**Problem:** `psql` command not recognized on Windows (not installed locally). + +**Solution:** Use Docker exec to run psql inside the container: +```powershell +# Single command +docker exec -it apoklisis_postgres psql -U postgres -c "SELECT * FROM users;" + +# Interactive session +docker exec -it apoklisis_postgres psql -U postgres + +# Create database +docker exec -it apoklisis_postgres psql -U postgres -c "CREATE DATABASE apoklisis_test;" +``` + +--- + +### Docker Container Not Running +**Problem:** Database connection errors. + +**Solution:** Check and start the container: +```powershell +docker ps # See running containers +docker start apoklisis_postgres # Start if stopped +``` + +--- + +## Git / Version Control + +### git add from Wrong Directory +**Problem:** `git add .` doesn't stage files in parent directories. + +**Solution:** Always run git commands from project root: +```powershell +cd C:\Dev\Projects\Apoklisis +git add . +git commit -m "Your message" +git push origin main +``` + +--- + +### Untracked .claude/ Folder +**Problem:** `.claude/` folder appears in untracked files. + +**Solution:** Either: +1. Add to `.gitignore` if you don't want to track it +2. Or `git add .claude/` if you want Claude Code settings in repo + +--- + +## Environment-Specific Notes + +### Windows Path Handling +- Python and Node handle forward slashes `/` fine on Windows +- Use `os.path.join()` or `pathlib.Path` for cross-platform compatibility +- Avoid hardcoding backslashes `\` in code + +### PowerShell vs CMD +- Some Node.js/Python tools work better in CMD than PowerShell +- If a command fails in PowerShell, try CMD or add to Desktop Commander config: + - `defaultShell: "cmd"` + +--- + +## API / Endpoint Patterns + +### JSONB Fields with Timestamps +**Problem:** Storing datetime objects directly in JSONB fields causes serialization errors. + +**Solution:** Convert to ISO string before storing: +```python +decision = { + "node_id": node_id, + "answer": answer, + "timestamp": datetime.now(timezone.utc).isoformat() # String, not datetime +} +``` + +--- + +### Soft Delete Pattern +**Pattern:** Use `is_active` boolean instead of actually deleting records. + +**Note:** Our schema uses `is_active`, not `is_deleted` (documentation was corrected on Jan 28, 2026). + +--- + +## Testing + +### Test Database Setup +**Requirement:** Tests need a separate database. + +**One-time setup:** +```powershell +docker exec -it apoklisis_postgres psql -U postgres -c "CREATE DATABASE apoklisis_test;" +``` + +**Run tests:** +```powershell +cd backend +pytest +``` + +--- + +## Common Mistakes to Avoid + +| Mistake | Correct Approach | +|---------|------------------| +| Using `datetime.utcnow()` | Use `datetime.now(timezone.utc)` | +| Running git from subdirectory | Always `cd` to project root first | +| Forgetting to activate venv | Check for `(venv)` prefix | +| Editing files in OneDrive folder | Use `C:\Dev\Projects\` | +| Using `psql` directly on Windows | Use `docker exec` instead | +| Storing datetime in JSON | Convert to `.isoformat()` string | + +--- + +## Adding New Lessons + +When you encounter and fix a bug, add it here with: +1. **Problem:** What error/symptom occurred +2. **Cause:** Why it happened (if known) +3. **Solution:** How to fix it +4. **Files affected:** Where to look (if applicable) diff --git a/PROGRESS.md b/PROGRESS.md index fa55733f..e3c08773 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -401,9 +401,49 @@ frontend/ --- +## Phase 2: Tree Editor (IN PROGRESS) + +### Tree Editor Implementation + +**Store**: `frontend/src/store/treeEditorStore.ts` +- Zustand with immer middleware for immutable updates +- zundo middleware for undo/redo functionality +- Node CRUD operations (add, update, delete) +- nodeMap for O(1) lookups +- Validation error tracking + +**Components Created**: + +| Component | Purpose | +|-----------|---------| +| `TreeEditorLayout.tsx` | Split-view container (editor 60%, preview 40%) | +| `TreeMetadataForm.tsx` | Tree name, description, category form | +| `NodeList.tsx` | List of nodes with add/edit/delete actions | +| `NodeEditorModal.tsx` | Modal wrapper for node editing | +| `NodeFormDecision.tsx` | Decision node fields (question, help_text, options) | +| `NodeFormAction.tsx` | Action node fields (title, description, commands) | +| `NodeFormResolution.tsx` | Solution node fields (title, steps) | +| `DynamicArrayField.tsx` | Reusable add/remove array input | +| `NodePicker.tsx` | Type-grouped dropdown for selecting next_node_id | +| `TreePreviewPanel.tsx` | Visual tree preview with SharedLinksMap | +| `TreePreviewNode.tsx` | Node cards with solution indicators | + +**Routes Added**: +- `/trees/new` - Create new tree +- `/trees/:id/edit` - Edit existing tree + +**Key Features**: +- Form-based editing with live preview +- NodePicker groups nodes by type (Decision/Action/Solution) +- Solution connection indicators (green checkmark badges) +- Shared node detection (shows when multiple nodes link to same target) +- Modal with scrollable content, fixed header/footer + +--- + ## What's Next -### Phase 1b: Pre-built Trees (COMPLETE - Hybrid Approach) +### Phase 1b: Pre-built Trees (Partial) **Seed Data Script**: `backend/scripts/seed_data.py` @@ -414,16 +454,10 @@ frontend/ 4. ๐Ÿ”ฒ Citrix VDA Registration - Not started 5. ๐Ÿ”ฒ AD Replication Issues - Not started -**Run Seed Script**: -```bash -cd backend -python -m scripts.seed_data -``` - ### Remaining Work -1. **Testing**: End-to-end testing of full workflow -2. **Polish**: UI refinements, error handling improvements +1. **Tree Editor Polish**: Validation, required fields, orphan detection +2. **User Preferences**: Dark mode, export format defaults 3. **More Trees**: Add remaining 4 trees from `TS-EXAMPLES.md` 4. **Deployment**: Set up CI/CD pipeline and deploy to Render/Railway @@ -444,15 +478,15 @@ python -m scripts.seed_data ## Notes for Next Session - โœ… Backend **fully tested** - all 18 endpoints working correctly -- โœ… **Integration tests** - 29 tests with full coverage (all passing) -- โœ… **Seed script created** with Password Reset tree (full implementation) +- โœ… **Integration tests** - 40+ tests with full coverage (all passing) - โœ… **Frontend COMPLETE** - Full React app with all core pages +- โœ… **Tree Editor IMPLEMENTED** - Form-based editing with visual preview - โœ… **Full workflow tested** - Register โ†’ Login โ†’ Browse โ†’ Navigate โ†’ Complete โ†’ Export all working - ๐Ÿ“ **Single-user focus** for MVP (team features are in schema but low priority) ### Recommended Next Steps -1. **Polish UI**: Add loading states, better error messages, keyboard shortcuts -2. **Add more trees**: Implement remaining 4 trees from `TS-EXAMPLES.md` -3. **Deploy**: Set up CI/CD pipeline and deploy to Render/Railway -4. **Mobile responsiveness**: Optimize for tablet/mobile use +1. **Tree Editor Validation**: Required fields, orphan node detection, save validation +2. **User Preferences**: Dark mode toggle, export format defaults +3. **Add more trees**: Implement remaining 4 trees from `TS-EXAMPLES.md` +4. **Deploy**: Set up CI/CD pipeline and deploy to Render/Railway diff --git a/README.md b/README.md index e5925a64..32ef1798 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ > Transform chaos into clarity - guided troubleshooting with automatic documentation for MSP engineers. -## Project Status: ๐Ÿ“‹ Planning Phase +## Project Status: ๐Ÿš€ Phase 2 - Active Development -Currently in the planning and architecture phase. Development will begin once key decisions are made and initial troubleshooting scenarios are documented. +**Backend**: Complete and tested (18 API endpoints, 40+ integration tests) +**Frontend**: Core features complete, Tree Editor in progress +**Tree Editor**: Visual editor with form-based editing and live preview panel --- @@ -315,39 +317,35 @@ Options being considered: ## Roadmap at a Glance ``` -โ””โ”€ [๐Ÿ“‹ Planning] โ† WE ARE HERE - โ”œโ”€ ๐Ÿ“ Document requirements +โ””โ”€ [โœ… Planning] COMPLETE + โ”œโ”€ โœ… Document requirements โ”œโ”€ โœ… Make key decisions - โ””โ”€ ๐Ÿ—๏ธ Setup initial architecture - -โ””โ”€ [๐Ÿš€ Week 1-3: MVP] - โ”œโ”€ Basic tree navigation - โ”œโ”€ Export functionality - โ””โ”€ 5 starter trees - -โ””โ”€ [๐Ÿ‘ฅ Week 4-6: Team Ready] - โ”œโ”€ Team management - โ”œโ”€ Tree editor - โ””โ”€ Mobile responsive - -โ””โ”€ [๐Ÿ’ผ Week 7-12: Professional] + โ””โ”€ โœ… Setup initial architecture + +โ””โ”€ [โœ… Phase 1: MVP] COMPLETE + โ”œโ”€ โœ… Backend API (18 endpoints) + โ”œโ”€ โœ… Tree navigation UI + โ”œโ”€ โœ… Session tracking + โ””โ”€ โœ… Export functionality + +โ””โ”€ [๐Ÿš€ Phase 2: Team Ready] โ† IN PROGRESS + โ”œโ”€ โœ… Tree Editor (form-based with preview) + โ”œโ”€ โณ Team management + โ””โ”€ โณ Mobile responsive + +โ””โ”€ [๐Ÿ“‹ Phase 3: Professional] โ”œโ”€ Attachments โ”œโ”€ Offline mode โ””โ”€ Analytics - -โ””โ”€ [๐Ÿ”Œ Month 4-6: Platform] + +โ””โ”€ [๐Ÿ“‹ Phase 4: Platform] โ”œโ”€ API & integrations โ”œโ”€ Automation โ””โ”€ Enterprise features - -โ””โ”€ [๐Ÿš€ Beyond: Growth] - โ”œโ”€ Marketplace - โ”œโ”€ AI features - โ””โ”€ Mobile apps ``` --- -**Last Updated:** 2026-01-22 -**Project Status:** Planning Phase -**Next Milestone:** Answer key questions, document first 3 troubleshooting scenarios +**Last Updated:** 2026-01-28 +**Project Status:** Phase 2 - Active Development +**Next Milestone:** Complete Tree Editor polishing, Team management features diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 87ccb640..297d56bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,11 +11,13 @@ "axios": "^1.13.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "immer": "^11.1.3", "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", + "zundo": "^2.3.0", "zustand": "^5.0.10" }, "devDependencies": { @@ -3039,6 +3041,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4790,6 +4802,24 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/zundo": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/zundo/-/zundo-2.3.0.tgz", + "integrity": "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/charkour" + }, + "peerDependencies": { + "zustand": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "zustand": { + "optional": false + } + } + }, "node_modules/zustand": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index adca9491..1e777196 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,11 +13,13 @@ "axios": "^1.13.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "immer": "^11.1.3", "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", + "zundo": "^2.3.0", "zustand": "^5.0.10" }, "devDependencies": { diff --git a/frontend/src/components/common/Modal.tsx b/frontend/src/components/common/Modal.tsx new file mode 100644 index 00000000..46408139 --- /dev/null +++ b/frontend/src/components/common/Modal.tsx @@ -0,0 +1,101 @@ +import { useEffect, useCallback, type ReactNode } from 'react' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ModalProps { + isOpen: boolean + onClose: () => void + title: string + children: ReactNode + /** Optional footer content that stays fixed at bottom (doesn't scroll) */ + footer?: ReactNode + size?: 'sm' | 'md' | 'lg' | 'xl' +} + +export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }: ModalProps) { + // Close on Escape key + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose() + } + }, + [onClose] + ) + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown) + document.body.style.overflow = 'hidden' + } + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.body.style.overflow = '' + } + }, [isOpen, handleKeyDown]) + + if (!isOpen) return null + + const sizeClasses = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-4xl', + } + + return ( +
+ {/* Backdrop */} + + ) +} + +export default Modal diff --git a/frontend/src/components/tree-editor/DynamicArrayField.tsx b/frontend/src/components/tree-editor/DynamicArrayField.tsx new file mode 100644 index 00000000..66c3b036 --- /dev/null +++ b/frontend/src/components/tree-editor/DynamicArrayField.tsx @@ -0,0 +1,112 @@ +import type { ReactNode } from 'react' +import { Plus, Trash2, ChevronUp, ChevronDown } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface DynamicArrayFieldProps { + items: T[] + onAdd: () => void + onRemove: (index: number) => void + onReorder?: (fromIndex: number, toIndex: number) => void + renderItem: (item: T, index: number) => ReactNode + addLabel?: string + maxItems?: number + minItems?: number + className?: string +} + +export function DynamicArrayField({ + items, + onAdd, + onRemove, + onReorder, + renderItem, + addLabel = 'Add Item', + maxItems, + minItems = 0, + className +}: DynamicArrayFieldProps) { + const canAdd = maxItems === undefined || items.length < maxItems + const canRemove = items.length > minItems + + const handleMoveUp = (index: number) => { + if (onReorder && index > 0) { + onReorder(index, index - 1) + } + } + + const handleMoveDown = (index: number) => { + if (onReorder && index < items.length - 1) { + onReorder(index, index + 1) + } + } + + return ( +
+ {items.map((item, index) => ( +
+ {/* Reorder buttons */} + {onReorder && items.length > 1 && ( +
+ + +
+ )} + + {/* Item content */} +
{renderItem(item, index)}
+ + {/* Remove button */} + {canRemove && ( + + )} +
+ ))} + + {/* Add button */} + {canAdd && ( + + )} + + {/* Empty state */} + {items.length === 0 && !canAdd && ( +

No items

+ )} +
+ ) +} + +export default DynamicArrayField diff --git a/frontend/src/components/tree-editor/NodeEditorModal.tsx b/frontend/src/components/tree-editor/NodeEditorModal.tsx new file mode 100644 index 00000000..3e266860 --- /dev/null +++ b/frontend/src/components/tree-editor/NodeEditorModal.tsx @@ -0,0 +1,85 @@ +import { Modal } from '@/components/common/Modal' +import { useTreeEditorStore } from '@/store/treeEditorStore' +import { NodeFormDecision } from './NodeFormDecision' +import { NodeFormAction } from './NodeFormAction' +import { NodeFormResolution } from './NodeFormResolution' +import type { TreeStructure } from '@/types' + +interface NodeEditorModalProps { + node: TreeStructure + onClose: () => void +} + +export function NodeEditorModal({ node, onClose }: NodeEditorModalProps) { + const { updateNode, validationErrors } = useTreeEditorStore() + const nodeErrors = validationErrors.filter(e => e.nodeId === node.id) + + const handleUpdate = (updates: Partial) => { + updateNode(node.id, updates) + } + + const getTitle = () => { + switch (node.type) { + case 'decision': + return 'Edit Decision Node' + case 'action': + return 'Edit Action Node' + case 'solution': + return 'Edit Solution Node' + default: + return 'Edit Node' + } + } + + const footerContent = ( +
+ +
+ ) + + return ( + + {/* Node ID display */} +
+ Node ID: {node.id} +
+ + {/* Validation errors */} + {nodeErrors.length > 0 && ( +
+ {nodeErrors.map((error, i) => ( +
+ {error.message} +
+ ))} +
+ )} + + {/* Type-specific form */} + {node.type === 'decision' && ( + + )} + {node.type === 'action' && ( + + )} + {node.type === 'solution' && ( + + )} +
+ ) +} + +export default NodeEditorModal diff --git a/frontend/src/components/tree-editor/NodeFormAction.tsx b/frontend/src/components/tree-editor/NodeFormAction.tsx new file mode 100644 index 00000000..6898e4a8 --- /dev/null +++ b/frontend/src/components/tree-editor/NodeFormAction.tsx @@ -0,0 +1,152 @@ +import { DynamicArrayField } from './DynamicArrayField' +import { NodePicker } from './NodePicker' +import { useTreeEditorStore } from '@/store/treeEditorStore' +import type { TreeStructure } from '@/types' +import { cn } from '@/lib/utils' + +interface NodeFormActionProps { + node: TreeStructure + onUpdate: (updates: Partial) => void +} + +export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) { + const { validationErrors } = useTreeEditorStore() + + const titleError = validationErrors.find( + e => e.nodeId === node.id && e.field === 'title' + ) + + const nextNodeError = validationErrors.find( + e => e.nodeId === node.id && e.field === 'next_node_id' + ) + + const handleAddCommand = () => { + onUpdate({ + commands: [...(node.commands || []), ''] + }) + } + + const handleRemoveCommand = (index: number) => { + const newCommands = [...(node.commands || [])] + newCommands.splice(index, 1) + onUpdate({ commands: newCommands }) + } + + const handleUpdateCommand = (index: number, value: string) => { + const newCommands = [...(node.commands || [])] + newCommands[index] = value + onUpdate({ commands: newCommands }) + } + + const handleReorderCommands = (fromIndex: number, toIndex: number) => { + const newCommands = [...(node.commands || [])] + const [moved] = newCommands.splice(fromIndex, 1) + newCommands.splice(toIndex, 0, moved) + onUpdate({ commands: newCommands }) + } + + return ( +
+ {/* Title */} +
+ + onUpdate({ title: e.target.value })} + placeholder="e.g., Restart the Service" + className={cn( + 'mt-1 block w-full rounded-md border px-3 py-2 text-sm', + 'bg-background text-foreground placeholder:text-muted-foreground', + 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary', + titleError ? 'border-destructive' : 'border-input' + )} + /> + {titleError && ( +

{titleError.message}

+ )} +
+ + {/* Description */} +
+ +