# Ghost Node — Frontend Migration Design Spec **Date:** 2026-03-11 **Author:** Claude (Session 16) **Status:** Approved by Abbas — Rev 4 (pragmatic: only use features that solve real problems) --- ## Overview Replace `dashboard.html` (3,515-line single-file vanilla JS dashboard) with a Next.js + React + TypeScript + Tailwind frontend. Next.js is used for its full capability set — not just routing. The Python FastAPI backend (`worker.py`, all endpoints, port 8000) is untouched during migration. The new frontend lives in `frontend/` alongside `dashboard.html` and is built tab-by-tab. When all 6 tabs are complete, FastAPI serves the Next.js build at `/`. --- ## Goals - Preserve cyberpunk terminal aesthetic while modernizing with Framer Motion animations - Replace global JS vars + `setInterval` polling with Zustand stores + TanStack Query - Use Next.js features only where they solve a real problem: image lazy-loading, font optimization, SSE for real-time engine state - Structure for SaaS multi-user expansion (auth route group + middleware placeholder) at zero cost now — no actual auth code until it's needed - Zero backend changes during migration — all FastAPI endpoints consumed as-is --- ## Tech Stack | Layer | Technology | Purpose | |---|---|---| | Framework | Next.js 14 (App Router) | Routing, API routes, SSE, Server Actions, image/font optimization | | Language | TypeScript | Type safety across all components | | Styling | Tailwind CSS | Cyberpunk theme as utility classes | | Components | shadcn/ui | Accessible Modal, Badge, Button, Toast primitives | | State | Zustand | Engine status + settings cache | | Data fetching | TanStack Query | Auto-refetch, caching, mutations | | Animations | Framer Motion | Entrance animations, price pulse, modal springs | | Drag-drop | @dnd-kit/core + @dnd-kit/sortable | Keywords and sites table reordering | | Icons | Lucide React | Replaces emoji indicators | | Fonts | next/font (Inter + JetBrains Mono) | Zero layout shift, self-hosted, 2 lines of code | | Images | next/image | Lazy-load 100+ listing thumbnails from external CDNs | | Real-time | Next.js API Route (SSE) | Pushes engine state to browser — eliminates all setInterval polling | | Auth (future) | NextAuth.js — deferred | Added when SaaS multi-user launches, slot reserved | | PWA, i18n | Deferred to SaaS phase | Not needed until multi-user hosting — adds complexity now for no gain | --- ## How Next.js Is Used (Only Where It Solves a Real Problem) ### 1. next/image — Lot Thumbnail Optimization Every `ListingRow` and `ImageGallery` uses `` from `next/image` instead of ``. Benefits for Ghost Node: - **Lazy loading** — only loads images when they scroll into view (critical: listings table can have 100+ rows each with images from external auction CDNs) - **Blur placeholder** — shows a blurred ghost-panel rectangle while the CDN image loads - **Size optimization** — Next.js resizes images to the exact rendered size (48×48 for thumbnails), saving bandwidth from HiBid/eBay CDNs - **Error handling** — `onError` falls back gracefully without crashing ```tsx {listing.title} e.currentTarget.style.display = 'none'} /> ``` ### 2. next/font — Zero Layout Shift Fonts loaded via `next/font/google` are self-hosted by Next.js at build time — no external Google Fonts request, no layout shift, no flash of unstyled text: ```ts // app/layout.tsx import { Inter, JetBrains_Mono } from 'next/font/google' const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' }) ``` Inter is used for labels, headings, nav. JetBrains Mono for prices, scores, timestamps, lot IDs — anything that looks better monospaced in a terminal-style dashboard. ### 3. Next.js API Routes — SSE Real-Time Stream `app/api/stream/route.ts` is a Next.js API route that opens a Server-Sent Events connection to the browser. It polls FastAPI's `/api/stats` and `/api/listings/countdown-sync` internally and pushes updates to connected clients — eliminating client-side polling loops. ```ts // app/api/stream/route.ts export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { while (true) { const stats = await fetch('http://localhost:8000/api/stats').then(r => r.json()) controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`)) await new Promise(r => setTimeout(r, 3000)) } } }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } }) } ``` Components subscribe via `EventSource('/api/stream')`. When Redis pub/sub is added, this route becomes a true push stream — no code changes needed in the components. ### 4. Next.js Middleware — Auth Guard Placeholder `middleware.ts` is a no-op now (passes all requests through). When NextAuth.js is added for multi-user SaaS, the auth guard drops in here — no component restructuring needed. Zero cost to include it now, saves a refactor later. **Deferred (added when actually needed, not before):** - **next-pwa** — push notifications + offline mode. Needs a push server. Add in SaaS phase. - **next-intl** — Arabic/English i18n. Add when multi-user hosting launches. - **BFF API routes** — TanStack Query handles parallel fetches cleanly. A BFF middleman adds debugging complexity for no gain at single-user scale. - **Server Actions** — TanStack mutations already give optimistic updates + error handling. Two mutation patterns in one codebase is inconsistent. - **Metadata API** — meaningless for a local dashboard. Add when public marketing site exists. --- ## Cyberpunk Theme Palette ```ts // tailwind.config.ts colors: { ghost: { bg: '#0a0e17', panel: '#111827', border: '#1f2937', accent: '#00ff88', gold: '#fbbf24', danger: '#ef4444', dim: '#6b7280', text: '#e5e7eb', } } ``` **shadcn/ui note:** Override shadcn's CSS variables in `globals.css` to use ghost palette. **RTL note:** All layout uses Tailwind's `rtl:` variant for Arabic direction support. --- ## Project Structure ``` ClaudeAuction2/ ├── worker.py / models.py / database.py / dashboard.html ← untouched during migration └── frontend/ ├── app/ │ ├── providers.tsx ← QueryClientProvider + Zustand (client component) │ ├── layout.tsx ← root layout: Header + Nav + StatusBar + fonts │ ├── page.tsx ← redirect → /dashboard │ ├── dashboard/page.tsx │ ├── listings/page.tsx │ ├── keywords/page.tsx │ ├── sites/page.tsx │ ├── settings/page.tsx │ ├── ai-log/page.tsx │ ├── (auth)/ ← NextAuth.js route group (placeholder) │ │ ├── login/page.tsx │ │ └── register/page.tsx │ └── api/ │ ├── stream/route.ts ← SSE real-time stream (engine state → browser) │ └── auth/[...nextauth]/ ← NextAuth.js placeholder (empty until SaaS) │ ├── components/ │ ├── layout/ │ │ ├── Header.tsx │ │ ├── Nav.tsx │ │ └── StatusBar.tsx │ ├── listings/ │ │ ├── ListingsTable.tsx │ │ ├── ListingRow.tsx │ │ ├── ListingDetailPanel.tsx │ │ └── ImageGallery.tsx │ ├── keywords/ │ │ ├── KeywordsTable.tsx │ │ └── KeywordRow.tsx │ ├── sites/ │ │ ├── SitesTable.tsx │ │ └── SiteRow.tsx │ └── ui/ ← shadcn/ui primitives │ ├── store/ │ ├── engineStore.ts │ └── settingsStore.ts │ ├── hooks/ │ ├── useListings.ts │ ├── useKeywords.ts │ ├── useSites.ts │ ├── useStats.ts │ ├── useCountdown.ts │ └── useSSE.ts ← EventSource('/api/stream') hook │ ├── lib/ │ ├── api/ │ │ ├── listings.ts │ │ ├── keywords.ts │ │ ├── sites.ts │ │ ├── config.ts │ │ ├── engine.ts │ │ ├── ai.ts │ │ └── system.ts │ └── types.ts │ ├── public/ │ └── icons/ ← Ghost Node favicon + app icon │ ├── middleware.ts ← auth guard (no-op now → NextAuth when SaaS) ├── next.config.ts ← rewrites + PWA + i18n config ├── tailwind.config.ts └── package.json ``` --- ## Architecture ### Client Component Boundary `providers.tsx` is the client boundary. It wraps `QueryClientProvider` and any Zustand hydration. All interactive components are Client Components (`"use client"`). `layout.tsx` stays a Server Component that imports `providers.tsx` as a child. ### Data Flow ``` FastAPI (port 8000) ↓ lib/api/*.ts wrappers (parse JSON fields before returning) TanStack Query hooks + Next.js BFF routes (with next/cache) ↓ typed, parsed data React components (all Client Components) ↓ mutations via Server Actions (forms) or TanStack mutations (real-time) FastAPI endpoints ↓ invalidate query cache / revalidatePath Components re-render ``` ### Real-Time Data Flow (SSE) ``` FastAPI /api/stats (polling inside Next.js API route) ↓ app/api/stream/route.ts (SSE) useSSE() hook (EventSource) ↓ writes to engineStore (Zustand) StatusBar + Dashboard tab re-render instantly ``` ### State Management | State | Where | Why | |---|---|---| | Listings | TanStack Query | Server state, background refetch | | Keywords | TanStack Query | Server state | | Sites | TanStack Query | Server state | | Engine status | Zustand engineStore (fed by SSE) | Shared across Header + StatusBar | | Config | Zustand settingsStore | No refetch on tab switch | | Countdown | useCountdown hook | Pure client-side ticker | --- ## API Coverage (lib/api/ mapping) | File | Endpoints | |---|---| | `listings.ts` | GET/DELETE /api/listings, countdown-sync, refresh-status. Export endpoints via `window.location`. | | `keywords.ts` | GET/POST/PUT/DELETE /api/keywords, reorder | | `sites.ts` | GET/POST/PUT/DELETE /api/sites, reorder, login, adapt, selectors | | `config.ts` | GET/POST /api/config | | `engine.ts` | pause/resume/restart/kill | | `ai.ts` | POST /api/ai/test, GET/DELETE /api/ai/debug/log | | `system.ts` | telegram test, backup download (`window.location`), backup restore (FormData), debug/db | Export endpoints (`/api/export/csv|json|html`) and backup download are triggered via `window.location.href` — browser downloads directly from FastAPI, bypassing Next.js proxy. --- ## TypeScript Types (lib/types.ts) ```ts 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 in lib/api/listings.ts closing_alerts_sent: number[] // parsed from JSON string in lib/api/listings.ts } interface Keyword { id: number term: string weight: number ai_target: string | null min_price: number | null max_price: number | null sort_order: number } 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 } // Fetched separately via GET /api/sites/{id}/selectors 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 } // Real fields from worker.py _stats + /api/stats handler interface Stats { total_scanned: number total_alerts: number last_cycle: string engine_status: 'Idle' | 'Running' | 'Paused' uptime_start: number uptime_seconds: number } interface Config { key: string value: string } ``` --- ## useCountdown Hook **State shape:** ```ts const [offsets, setOffsets] = useState>({}) const [syncedAt, setSyncedAt] = useState(Date.now()) ``` **Behaviour:** 1. Mount: fetch `/api/listings/countdown-sync` → populate offsets, record syncedAt 2. Every 1s: `currentMins = offsets[id] - (Date.now() - syncedAt) / 60000` 3. Every 60s: re-fetch sync endpoint → refresh offsets + syncedAt 4. New listings from `useListings` refetch that aren't in offsets map → return `null`, component falls back to raw `time_left` string 5. Returns `(id: number) => number | null` --- ## Loading and Error States | State | UI | |---|---| | Loading | Skeleton rows — ghost-panel bg, shimmer pulse animation | | Backend unreachable | Red banner: "ENGINE OFFLINE — cannot reach localhost:8000" + retry | | Empty | Dim: "NO LISTINGS CAPTURED YET" (per tab equivalent) | | Mutation failure | shadcn Toast: `[OPERATION] failed — [error]` in ghost-danger color | StatusBar shows "OFFLINE" in red if `/api/stream` SSE disconnects. --- ## Key Components ### StatusBar.tsx — Engine state, uptime, scanned, alerts. Fed by SSE via useSSE → engineStore. ### ListingRow.tsx — Thumbnail (next/image), countdown, price, AI badge, score. Click → detail panel. ### ImageGallery.tsx — Up to 10 images, next/image, click → new tab. Hidden when empty. ### KeywordRow.tsx — dnd-kit drag, inline edit, 💰 price filter, 🤖 AI target modal. ### SiteRow.tsx — Health badge, confidence (useSiteSelectors), adapt button, full edit modal. --- ## Animations (Framer Motion) | Interaction | Animation | |---|---| | Listings load | Stagger slide-in from bottom (0.05s per row) | | New listing | Pulse green border 2s | | Price update | Gold flash on price cell | | Modal | Scale + fade spring 0.3s | | Tab switch | Cross-fade 0.15s | | Drag row | Spring physics + shadow lift | | Closing-soon | Red pulse on time_left badge | --- ## Migration Strategy | Phase | Build | Acceptance Criteria | |---|---|---| | 0 | Scaffold: Next.js, Tailwind theme, providers, layout shell, SSE route, PWA, i18n skeleton | Port 3000 starts, nav renders in cyberpunk, StatusBar shows live engine state via SSE | | 1 | Dashboard tab | Stats cards show live totals, activity log auto-scrolls, engine controls work | | 2 | Listings tab | Table with thumbnails (next/image), countdown ticks, detail panel, delete works | | 3 | Keywords tab | CRUD, inline edit, drag-drop reorder, AI modal with live test, price filter | | 4 | Sites tab | CRUD, adapt triggers, confidence badge, health badge, edit modal | | 5 | Settings tab | All config keys load/save, telegram test, backup download/restore | | 6 | AI Log tab | Log cards stream via SSE, filter buttons, clear works | | 7 | FastAPI serves Next.js build at `/`, dashboard.html retired | All tabs functional via :8000 | ### Phase 7 — Production Serving `output: 'export'` produces static build in `frontend/out/`. FastAPI mounts it: ```python # worker.py — Phase 7 addition from fastapi.staticfiles import StaticFiles app.mount("/", StaticFiles(directory="frontend/out", html=True), name="static") ``` When multi-user auth is added, switch to Next.js as a Node server (remove `output: 'export'`, run `next start` on port 3000, Nginx proxies `/api` to FastAPI and `/` to Next.js). --- ## Future-Proofing Notes (Deferred — Add When Actually Needed) - **WebSocket** — swap `app/api/stream/route.ts` from SSE to WebSocket; components don't change - **PWA + push notifications** — add `next-pwa` when push server infrastructure exists (SaaS phase) - **Arabic i18n** — add `next-intl` when multi-user SaaS launches; Tailwind `rtl:` variant handles layout - **Auth** — `middleware.ts` guard + `(auth)/` login/register pages; zero restructuring needed - **Docker** — `docker-compose.yml` with `python:3.12-slim` + `node:20-alpine` services --- *Spec written: 2026-03-11 — Session 16 (Rev 3 — Next.js full capabilities)*