# 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 `<Image>` from `next/image` instead of `<img>`.
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
<Image
  src={listing.images[0]}
  alt={listing.title}
  width={48} height={48}
  className="object-cover rounded"
  placeholder="blur"
  blurDataURL="data:image/png;base64,..."
  onError={(e) => 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<Record<number, number>>({})
const [syncedAt, setSyncedAt] = useState<number>(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)*
