# Ghost Node Frontend Migration Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace dashboard.html with a Next.js + React + TypeScript + Tailwind frontend, tab-by-tab, while the Python backend runs untouched on port 8000. **Architecture:** Next.js App Router frontend in `frontend/` alongside existing files. All API calls proxy to FastAPI on port 8000 via Next.js rewrites. Built phase-by-phase — old dashboard.html stays live until Phase 7. **Tech Stack:** Next.js 14, React 18, TypeScript, Tailwind CSS, shadcn/ui, Zustand, TanStack Query, Framer Motion, @dnd-kit, Lucide React, next/image, next/font, Vitest + React Testing Library **Spec:** `docs/superpowers/specs/2026-03-11-frontend-migration-design.md` --- ## Chunk 1: Phase 0 — Scaffold ### Task 1: Prerequisites + Project Init **Files:** - Create: `frontend/package.json` (via npx) - Create: `frontend/next.config.ts` - Create: `frontend/tsconfig.json` (via npx) - [ ] **Step 1: Verify Node.js is installed** ```bash node --version npm --version ``` Expected: Node 18+ and npm 9+. If not installed, download from nodejs.org. - [ ] **Step 2: Create Next.js app** ```bash cd "C:/Users/Abbas/Documents/Downloads/ClaudeAuction2" npx create-next-app@latest frontend --typescript --tailwind --eslint --app --no-src-dir --import-alias "@/*" ``` When prompted, accept all defaults. - [ ] **Step 3: Install all additional dependencies** ```bash cd frontend npm install zustand @tanstack/react-query framer-motion @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities lucide-react npm install class-variance-authority clsx tailwind-merge npm install @radix-ui/react-dialog @radix-ui/react-toast @radix-ui/react-slot ``` - [ ] **Step 4: Install dev/test dependencies** ```bash npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom ``` - [ ] **Step 5: Add vitest config to package.json** Open `frontend/package.json` and add to the `"scripts"` section: ```json "test": "vitest", "test:ui": "vitest --ui" ``` And add after `"devDependencies"`: ```json , "vitest": { "environment": "jsdom", "globals": true, "setupFiles": ["./vitest.setup.ts"] } ``` - [ ] **Step 6: Create vitest setup file** Create `frontend/vitest.setup.ts`: ```ts import '@testing-library/jest-dom' ``` - [ ] **Step 7: Commit** ```bash cd "C:/Users/Abbas/Documents/Downloads/ClaudeAuction2" git add frontend/ git commit -m "feat: scaffold Next.js frontend project" ``` --- ### Task 2: Configure next.config.ts + Tailwind **Files:** - Modify: `frontend/next.config.ts` - Modify: `frontend/tailwind.config.ts` - Modify: `frontend/app/globals.css` - [ ] **Step 1: Write failing test for theme colors** Create `frontend/__tests__/theme.test.ts`: ```ts import { describe, it, expect } from 'vitest' import resolveConfig from 'tailwindcss/resolveConfig' import tailwindConfig from '../tailwind.config' const config = resolveConfig(tailwindConfig as any) describe('Ghost Node theme', () => { it('has ghost accent color defined', () => { expect((config.theme.colors as any).ghost?.accent).toBe('#00ff88') }) it('has ghost bg color defined', () => { expect((config.theme.colors as any).ghost?.bg).toBe('#0a0e17') }) }) ``` - [ ] **Step 2: Run test — expect FAIL** ```bash cd frontend && npm test -- --run theme ``` - [ ] **Step 3: Replace frontend/next.config.ts** ```ts import type { NextConfig } from 'next' const nextConfig: NextConfig = { async rewrites() { return [ { source: '/api/:path*', destination: 'http://localhost:8000/api/:path*' }, ] }, images: { remotePatterns: [ { protocol: 'https', hostname: '**' }, { protocol: 'http', hostname: '**' }, ], }, } export default nextConfig ``` - [ ] **Step 4: Replace frontend/tailwind.config.ts** ```ts import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { ghost: { bg: '#0a0e17', panel: '#111827', border: '#1f2937', accent: '#00ff88', gold: '#fbbf24', danger: '#ef4444', dim: '#6b7280', text: '#e5e7eb', }, }, fontFamily: { sans: ['var(--font-inter)', 'sans-serif'], mono: ['var(--font-jetbrains)', 'monospace'], }, }, }, plugins: [], } export default config ``` - [ ] **Step 5: Replace frontend/app/globals.css** ```css @tailwind base; @tailwind components; @tailwind utilities; /* Override shadcn/ui CSS variables to match Ghost Node cyberpunk palette */ :root { --background: 10 14 23; /* ghost-bg */ --foreground: 229 231 235; /* ghost-text */ --card: 17 24 39; /* ghost-panel */ --card-foreground: 229 231 235; --primary: 0 255 136; /* ghost-accent */ --primary-foreground: 10 14 23; --destructive: 239 68 68; /* ghost-danger */ --border: 31 41 55; /* ghost-border */ --muted: 107 114 128; /* ghost-dim */ --radius: 0.25rem; } * { box-sizing: border-box; } body { background-color: #0a0e17; color: #e5e7eb; font-family: var(--font-inter), sans-serif; } /* Scrollbar styling — cyberpunk */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: #111827; } ::-webkit-scrollbar-thumb { background: #1f2937; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #00ff88; } ``` - [ ] **Step 6: Run test — expect PASS** ```bash npm test -- --run theme ``` - [ ] **Step 7: Commit** ```bash cd .. git add frontend/next.config.ts frontend/tailwind.config.ts frontend/app/globals.css frontend/__tests__/theme.test.ts frontend/vitest.setup.ts git commit -m "feat: configure Next.js proxy, cyberpunk Tailwind theme, shadcn overrides" ``` --- ### Task 3: TypeScript Types **Files:** - Create: `frontend/lib/types.ts` - Create: `frontend/__tests__/types.test.ts` - [ ] **Step 1: Write type shape test** Create `frontend/__tests__/types.test.ts`: ```ts import { describe, it, expectTypeOf } from 'vitest' import type { Listing, Keyword, TargetSite, Stats, Config, SiteSelectors } from '../lib/types' describe('Types', () => { it('Listing has parsed images as string array', () => { expectTypeOf().toEqualTypeOf() }) it('Listing has closing_alerts_sent as number array', () => { expectTypeOf().toEqualTypeOf() }) it('TargetSite enabled is 0|1 not boolean', () => { expectTypeOf().toEqualTypeOf<0 | 1>() }) it('Stats engine_status is string union', () => { expectTypeOf().toEqualTypeOf<'Idle' | 'Running' | 'Paused'>() }) }) ``` - [ ] **Step 2: Run — expect type errors (types don't exist yet)** ```bash cd frontend && npm test -- --run types ``` - [ ] **Step 3: Create frontend/lib/types.ts** ```ts export interface Listing { id: number title: string price: number | null currency: string price_raw: string price_usd: number | null time_left: string time_left_mins: number | null link: string score: number keyword: string site_name: string timestamp: string price_updated_at: string | null ai_match: 1 | 0 | null ai_reason: string | null location: string | null images: string[] // parsed from JSON string at API layer closing_alerts_sent: number[] // parsed from JSON string at API layer } export interface Keyword { id: number term: string weight: number ai_target: string | null min_price: number | null max_price: number | null sort_order: number } export interface TargetSite { id: number name: string url_template: string search_selector: string enabled: 0 | 1 max_pages: number last_error: string | null error_count: number consecutive_failures: number last_success_at: string | null cooldown_until: string | null requires_login: 0 | 1 login_url: string | null login_check_selector: string | null login_enabled: 0 | 1 sort_order: number } export interface SiteSelectors { site_id: number confidence: number container_sel: string | null title_sel: string | null price_sel: string | null time_sel: string | null link_sel: string | null next_page_sel: string | null stale: boolean provider: 'groq' | 'ollama' | null generated_at: string | null } export interface Stats { total_scanned: number total_alerts: number last_cycle: string engine_status: 'Idle' | 'Running' | 'Paused' uptime_start: number uptime_seconds: number } export interface Config { key: string value: string } ``` - [ ] **Step 4: Run — expect PASS** ```bash npm test -- --run types ``` - [ ] **Step 5: Commit** ```bash cd .. git add frontend/lib/types.ts frontend/__tests__/types.test.ts git commit -m "feat: add TypeScript types for all Ghost Node entities" ``` --- ### Task 4: Engine Store + SSE Hook **Files:** - Create: `frontend/store/engineStore.ts` - Create: `frontend/hooks/useSSE.ts` - Create: `frontend/app/api/stream/route.ts` - Create: `frontend/__tests__/engineStore.test.ts` - [ ] **Step 1: Write failing store test** Create `frontend/__tests__/engineStore.test.ts`: ```ts import { describe, it, expect, beforeEach } from 'vitest' import { useEngineStore } from '../store/engineStore' describe('engineStore', () => { beforeEach(() => { useEngineStore.setState({ status: 'Idle', uptime_seconds: 0, total_scanned: 0, total_alerts: 0, last_cycle: 'Never', isOffline: false, }) }) it('initial state is Idle', () => { expect(useEngineStore.getState().status).toBe('Idle') }) it('setStats updates all fields', () => { useEngineStore.getState().setStats({ engine_status: 'Running', uptime_seconds: 120, total_scanned: 42, total_alerts: 3, last_cycle: '2026-03-11T10:00:00', uptime_start: Date.now() / 1000, }) const s = useEngineStore.getState() expect(s.status).toBe('Running') expect(s.total_scanned).toBe(42) }) it('setOffline marks isOffline true', () => { useEngineStore.getState().setOffline(true) expect(useEngineStore.getState().isOffline).toBe(true) }) }) ``` - [ ] **Step 2: Run — expect FAIL** ```bash cd frontend && npm test -- --run engineStore ``` - [ ] **Step 3: Create frontend/store/engineStore.ts** ```ts import { create } from 'zustand' import type { Stats } from '@/lib/types' interface EngineState { status: 'Idle' | 'Running' | 'Paused' uptime_seconds: number total_scanned: number total_alerts: number last_cycle: string isOffline: boolean setStats: (stats: Stats) => void setOffline: (offline: boolean) => void } export const useEngineStore = create((set) => ({ status: 'Idle', uptime_seconds: 0, total_scanned: 0, total_alerts: 0, last_cycle: 'Never', isOffline: false, setStats: (stats) => set({ status: stats.engine_status, uptime_seconds: stats.uptime_seconds, total_scanned: stats.total_scanned, total_alerts: stats.total_alerts, last_cycle: stats.last_cycle, isOffline: false, }), setOffline: (offline) => set({ isOffline: offline }), })) ``` - [ ] **Step 4: Run — expect PASS** ```bash npm test -- --run engineStore ``` - [ ] **Step 5: Create frontend/app/api/stream/route.ts** ```ts export const dynamic = 'force-dynamic' export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { const send = (data: object) => { controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)) } // Poll FastAPI stats every 3s and push to browser while (true) { try { const res = await fetch('http://localhost:8000/api/stats', { signal: AbortSignal.timeout(2000), }) if (res.ok) { const stats = await res.json() send({ type: 'stats', payload: stats }) } } catch { send({ type: 'offline' }) } await new Promise((r) => setTimeout(r, 3000)) } }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }) } ``` - [ ] **Step 6: Create frontend/hooks/useSSE.ts** ```ts 'use client' import { useEffect } from 'react' import { useEngineStore } from '@/store/engineStore' export function useSSE() { const setStats = useEngineStore((s) => s.setStats) const setOffline = useEngineStore((s) => s.setOffline) useEffect(() => { const es = new EventSource('/api/stream') es.onmessage = (e) => { try { const msg = JSON.parse(e.data) if (msg.type === 'stats') setStats(msg.payload) if (msg.type === 'offline') setOffline(true) } catch {} } es.onerror = () => setOffline(true) return () => es.close() }, [setStats, setOffline]) } ``` - [ ] **Step 7: Commit** ```bash cd .. git add frontend/store/engineStore.ts frontend/hooks/useSSE.ts frontend/app/api/stream/route.ts frontend/__tests__/engineStore.test.ts git commit -m "feat: engine Zustand store + SSE API route + useSSE hook" ``` --- ### Task 5: Layout Shell (Header, Nav, StatusBar, Providers) **Files:** - Create: `frontend/app/providers.tsx` - Create: `frontend/app/layout.tsx` - Create: `frontend/app/page.tsx` - Create: `frontend/components/layout/Header.tsx` - Create: `frontend/components/layout/Nav.tsx` - Create: `frontend/components/layout/StatusBar.tsx` - Create: `frontend/lib/utils.ts` - Create: `frontend/middleware.ts` - [ ] **Step 1: Write StatusBar render test** Create `frontend/__tests__/StatusBar.test.tsx`: ```tsx import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen } from '@testing-library/react' import { useEngineStore } from '../store/engineStore' import StatusBar from '../components/layout/StatusBar' // useSSE does nothing in tests vi.mock('../hooks/useSSE', () => ({ useSSE: () => {} })) describe('StatusBar', () => { beforeEach(() => { useEngineStore.setState({ status: 'Running', uptime_seconds: 3661, total_scanned: 99, total_alerts: 5, last_cycle: 'Never', isOffline: false, setStats: vi.fn(), setOffline: vi.fn(), }) }) it('shows engine status', () => { render() expect(screen.getByText(/RUNNING/i)).toBeTruthy() }) it('formats uptime correctly', () => { render() expect(screen.getByText(/1h 1m/i)).toBeTruthy() }) it('shows offline banner when isOffline', () => { useEngineStore.setState({ isOffline: true } as any) render() expect(screen.getByText(/OFFLINE/i)).toBeTruthy() }) }) ``` - [ ] **Step 2: Run — expect FAIL** ```bash cd frontend && npm test -- --run StatusBar ``` - [ ] **Step 3: Create frontend/lib/utils.ts** ```ts import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } export function formatUptime(seconds: number): string { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) return `${h}h ${m}m` } ``` - [ ] **Step 4: Create frontend/components/layout/StatusBar.tsx** ```tsx 'use client' import { useEngineStore } from '@/store/engineStore' import { useSSE } from '@/hooks/useSSE' import { formatUptime } from '@/lib/utils' export default function StatusBar() { useSSE() // connects SSE on mount const { status, uptime_seconds, total_scanned, total_alerts, isOffline } = useEngineStore() if (isOffline) { return (
ENGINE OFFLINE — cannot reach localhost:8000
) } const statusColor = status === 'Running' ? 'text-ghost-accent' : status === 'Paused' ? 'text-ghost-gold' : 'text-ghost-dim' return (
ENGINE: {status.toUpperCase()} UPTIME: {formatUptime(uptime_seconds)} SCANNED: {total_scanned} ALERTS: {total_alerts}
) } ``` - [ ] **Step 5: Run StatusBar test — expect PASS** ```bash npm test -- --run StatusBar ``` - [ ] **Step 6: Create frontend/components/layout/Nav.tsx** ```tsx 'use client' import Link from 'next/link' import { usePathname } from 'next/navigation' import { cn } from '@/lib/utils' const TABS = [ { href: '/dashboard', label: 'DASHBOARD', icon: '📡' }, { href: '/listings', label: 'LISTINGS', icon: '🎯' }, { href: '/keywords', label: 'KEYWORDS', icon: '🔍' }, { href: '/sites', label: 'TARGET SITES', icon: '🌐' }, { href: '/settings', label: 'SETTINGS', icon: '⚙️' }, { href: '/ai-log', label: 'AI LOG', icon: '🧠' }, ] export default function Nav() { const pathname = usePathname() return ( ) } ``` - [ ] **Step 7: Create frontend/components/layout/Header.tsx** ```tsx 'use client' import { useEngineStore } from '@/store/engineStore' const API = 'http://localhost:8000' export default function Header() { const status = useEngineStore((s) => s.status) const call = (path: string) => fetch(`${API}/api/engine/${path}`, { method: 'POST' }) return (
👻
GHOST NODE
AUCTION SNIPER v2.5
) } ``` - [ ] **Step 8: Create frontend/app/providers.tsx** ```tsx 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' export default function Providers({ children }: { children: React.ReactNode }) { const [client] = useState(() => new QueryClient({ defaultOptions: { queries: { staleTime: 5000, retry: 1 } }, })) return {children} } ``` - [ ] **Step 9: Replace frontend/app/layout.tsx** ```tsx import type { Metadata } from 'next' import { Inter, JetBrains_Mono } from 'next/font/google' import './globals.css' import Providers from './providers' import Header from '@/components/layout/Header' import Nav from '@/components/layout/Nav' import StatusBar from '@/components/layout/StatusBar' const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-jetbrains' }) export const metadata: Metadata = { title: 'Ghost Node — Auction Sniper', } export default function RootLayout({ children }: { children: React.ReactNode }) { return (