# Plan: Fix Galaxy Node Clicking and Navigation **Priority:** HIGH - Critical user experience issue **Dependencies:** None **Affects:** Galaxy visualization navigation, user frustration ## Overview There are two critical bugs with galaxy node interaction: 1. When going directly to a node ID link (`/galaxy/node:xxx`), it redirects to `/chat` 2. When clicking on a node in `/galaxy` (either general or on a specific node ID URL), the modal closes automatically Both issues severely impact the galaxy user experience and need immediate fixing. ## Current Broken Behavior ### Issue 1: Direct Node URL Redirects to Chat ``` User clicks: /galaxy/node:abc123 Expected: Shows node detail page with full content Actual: Redirects to /chat ``` ### Issue 2: Modal Auto-Closes on Node Click ``` User action: Click node sphere in galaxy visualization Expected: Modal stays open showing node details Actual: Modal opens briefly then closes immediately ``` ## Root Cause Analysis Let me investigate the current implementation: ### 1. Check `/app/galaxy/[node-id]/page.tsx` Likely issues: - Missing authentication check causing redirect - Incorrect route parameter parsing - Navigation logic in wrong place ### 2. Check Galaxy Modal Component Likely issues: - Event handler conflict (click bubbling to parent) - State management issue (modal state reset) - React Three Fiber event handling bug ## Proposed Fixes ### Fix 1: Node Detail Page Route #### Current (broken): ```typescript // app/galaxy/[node-id]/page.tsx export default async function NodeDetailPage({ params }: { params: { 'node-id': string } }) { // Redirects to /chat if not authenticated? } ``` #### Fixed: ```typescript // app/galaxy/[node-id]/page.tsx import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { verifySurrealJwt } from '@/lib/auth/jwt'; import { connectToDB } from '@/lib/db'; export default async function NodeDetailPage({ params, }: { params: { 'node-id': string }; }) { // 1. Verify authentication const cookieStore = await cookies(); const surrealJwt = cookieStore.get('ponderants-auth')?.value; if (!surrealJwt) { // Redirect to login, preserving the intended destination redirect(`/login?redirect=/galaxy/${params['node-id']}`); } const userSession = verifySurrealJwt(surrealJwt); if (!userSession) { redirect(`/login?redirect=/galaxy/${params['node-id']}`); } // 2. Fetch node data const db = await connectToDB(); const nodeId = params['node-id']; const nodes = await db.select(nodeId); const node = nodes[0]; if (!node) { // Node doesn't exist or user doesn't have access redirect('/galaxy?error=node-not-found'); } // 3. Verify user owns this node if (node.user_did !== userSession.did) { redirect('/galaxy?error=unauthorized'); } // 4. Render node detail page return ( {node.title} {node.body} {node.atp_uri && ( View on Bluesky )} ); } ``` ### Fix 2: Galaxy Modal Component #### Problem: Click event bubbling When user clicks node in R3F, the click event might be: - Bubbling to parent canvas - Triggering canvas click handler that closes modal - Or: React state update race condition #### Solution: Stop event propagation ```typescript // components/galaxy/NodeSphere.tsx import { useRef } from 'react'; import { ThreeEvent } from '@react-three/fiber'; function NodeSphere({ node, onClick }: { node: NodeData; onClick: (node: NodeData) => void }) { const meshRef = useRef(); const handleClick = (event: ThreeEvent) => { // CRITICAL: Stop propagation to prevent canvas click handler event.stopPropagation(); // Call parent onClick onClick(node); }; return ( ); } ``` #### Problem: Modal state management Modal might be controlled by both: - Galaxy component state - URL query params - Leading to race condition #### Solution: Single source of truth ```typescript // app/galaxy/page.tsx 'use client'; import { useState, useEffect } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; export default function GalaxyPage() { const searchParams = useSearchParams(); const router = useRouter(); // Modal state controlled ONLY by URL query param const selectedNodeId = searchParams.get('node'); const handleNodeClick = (nodeId: string) => { // Update URL to open modal router.push(`/galaxy?node=${encodeURIComponent(nodeId)}`, { scroll: false }); }; const handleModalClose = () => { // Remove query param to close modal router.push('/galaxy', { scroll: false }); }; return ( <> {selectedNodeId && } ); } ``` ### Fix 3: Canvas Click Handler If canvas has a click handler that closes modal, remove it or add condition: ```typescript // components/galaxy/ThoughtGalaxy.tsx { // Only close modal if clicking on background (not a node) if (e.target === e.currentTarget) { onBackgroundClick?.(); } }} > {/* ... */} ``` ## Testing Strategy ### Manual Playwright MCP Testing #### Test 1: Direct Node URL ```typescript test('Direct node URL loads correctly', async ({ page }) => { // Create a node first const nodeId = await createTestNode(page); // Navigate directly to node detail page await page.goto(`/galaxy/${nodeId}`); // Verify we're on the correct page await expect(page).toHaveURL(/\/galaxy\/node:/); await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); await expect(page.getByText(/View on Bluesky/)).toBeVisible(); }); ``` #### Test 2: Modal Stays Open ```typescript test('Galaxy modal stays open when clicking node', async ({ page }) => { await page.goto('/galaxy'); // Click on a node sphere await page.evaluate(() => { const event = new CustomEvent('node-click', { detail: { nodeId: 'node:123' } }); window.dispatchEvent(event); }); // Modal should open await expect(page.getByRole('dialog')).toBeVisible(); // Wait 2 seconds await page.waitForTimeout(2000); // Modal should still be open await expect(page.getByRole('dialog')).toBeVisible(); }); ``` #### Test 3: Modal Navigation ```typescript test('Can navigate from galaxy to node detail page', async ({ page }) => { await page.goto('/galaxy'); // Click node to open modal await clickNode(page, 'node:123'); // Modal opens await expect(page.getByRole('dialog')).toBeVisible(); // Click "View Full Detail" await page.click('button:has-text("View Full Detail")'); // Navigate to detail page await expect(page).toHaveURL(/\/galaxy\/node:123/); await expect(page.getByRole('dialog')).not.toBeVisible(); }); ``` ### Magnitude Tests ```typescript import { test } from 'magnitude-test'; test('User can navigate to node via direct URL', async (agent) => { // Create a node first await agent.open('http://localhost:3000/chat'); await agent.act('Create a test node'); const nodeUrl = await agent.getData('node-url'); // Navigate directly to node URL await agent.open(nodeUrl); await agent.check('Node detail page loads'); await agent.check('Node title is visible'); await agent.check('Node body is visible'); await agent.check('Bluesky link is visible'); }); test('Galaxy modal stays open when clicking nodes', async (agent) => { await agent.open('http://localhost:3000/galaxy'); await agent.act('Click on a node sphere'); await agent.check('Modal opens'); await agent.wait(2000); await agent.check('Modal is still open'); await agent.check('Node details are visible'); }); test('Can navigate from galaxy modal to node detail page', async (agent) => { await agent.open('http://localhost:3000/galaxy'); await agent.act('Click on a node'); await agent.check('Modal opens'); await agent.act('Click "View Full Detail" button'); await agent.check('Navigated to node detail page'); await agent.check('URL contains node ID'); await agent.check('Full node content is visible'); }); ``` ## Implementation Steps 1. **Fix node detail page route** - Add proper authentication check - Add redirect with return URL - Add node ownership verification - Add error handling for missing nodes 2. **Fix modal event handling** - Add `event.stopPropagation()` to node click handler - Remove or condition any canvas-level click handlers - Ensure modal state is controlled by URL query param 3. **Test with Playwright MCP** - Test direct URL navigation - Test modal stays open - Test modal to detail page navigation - Fix any issues found 4. **Add Magnitude tests** - Write tests covering all 3 scenarios - Ensure tests pass 5. **Deploy and verify in production** ## Success Criteria - ✅ Direct node URLs (`/galaxy/node:xxx`) load correctly - ✅ No redirect to `/chat` when accessing node URLs - ✅ Galaxy modal stays open after clicking node - ✅ Can navigate from modal to detail page - ✅ Can navigate from detail page back to galaxy - ✅ All Playwright MCP tests pass - ✅ All Magnitude tests pass - ✅ No console errors during navigation ## Files to Update 1. `app/galaxy/[node-id]/page.tsx` - Fix authentication and routing 2. `app/galaxy/page.tsx` - Fix modal state management 3. `components/galaxy/ThoughtGalaxy.tsx` - Fix canvas click handling 4. `components/galaxy/NodeSphere.tsx` - Fix node click event propagation ## Files to Create 1. `tests/playwright/galaxy-navigation.spec.ts` - Manual tests 2. `tests/magnitude/galaxy-navigation.mag.ts` - Magnitude tests