# Frontend Replacement Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the Go template-based frontend with the search-zen-50 React SPA, embedded in the Go binary as a single deployment.

**Architecture:** Build React app → embed in Go binary via `//go:embed` → serve via Go HTTP server with SPA fallback routing. React calls `/search?format=json` and `/autocompleter?q=` APIs.

**Tech Stack:** Go (embed), React 18, Vite, TailwindCSS, React Router, @tanstack/react-query

---

## File Map

| File | Action |
|------|--------|
| `cmd/kafka/main.go` | Modify - replace template handlers with SPA handler |
| `internal/spa/spa.go` | Create - embed React build, serve static files, SPA fallback |
| `internal/spa/dist/` | Build output - React build artifacts (gitignored) |
| `src/hooks/use-search.ts` | Modify - replace mock with real API calls |
| `src/lib/mock-data.ts` | Keep types, remove MOCK_RESPONSE usage |

---

## Task 1: Build React App

**Files:**
- Build: `/tmp/search-zen-50/dist/` (output directory)

- [ ] **Step 1: Install dependencies and build**

```bash
cd /tmp/search-zen-50 && bun install && bun run build
```

Expected: `dist/` directory created with `index.html`, `assets/` folder containing JS/CSS bundles

- [ ] **Step 2: Verify dist contents**

```bash
ls /tmp/search-zen-50/dist/ && ls /tmp/search-zen-50/dist/assets/ | head -10
```

Expected: `index.html` exists, `assets/` contains `.js` and `.css` files

---

## Task 2: Create SPA Go Package

**Files:**
- Create: `internal/spa/spa.go`

```go
package spa

import (
	"embed"
	"io/fs"
	"net/http"
	"path"
)

//go:embed all:dist
var distFS embed.FS

// DistFS returns the embedded dist directory as an fs.FS.
func DistFS() (fs.FS, error) {
	return fs.Sub(distFS, "dist")
}

// NewHandler returns an HTTP handler that:
// - Serves static files from the embedded dist/ directory
// - Falls back to index.html for SPA routing (any non-API path)
func NewHandler() http.Handler {
	dist, err := DistFS()
	if err != nil {
		panic("spa: embedded dist not found: " + err.Error())
	}
	return &spaHandler{dist: dist}
}

type spaHandler struct {
	dist fs.FS
}

func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// API paths are handled by Go API handlers - this should never be reached
	// since Go mux dispatches to specific handlers first. But if reached,
	// pass through to FileServer which will return 404 for unknown paths.

	// Try to serve the requested file first
	filePath := path.Clean(r.URL.Path)
	f, err := h.dist.Open(filePath)
	if err == nil {
		f.Close()
		// File exists - serve it via FileServer
		http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
		return
	}

	// Fallback to index.html for SPA routing
	indexFile, err := h.dist.Open("index.html")
	if err != nil {
		http.Error(w, "index.html not found in embedded files", http.StatusInternalServerError)
		return
	}
	indexFile.Close()
	http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
}

```

---

## Task 3: Wire SPA Handler in main.go

**Files:**
- Modify: `cmd/kafka/main.go`

- [ ] **Step 1: Replace handlers with SPA**

In `main.go`, find and replace the `mux.HandleFunc` section (lines 82-88) and the static file serving section (lines 90-96).

Old code (lines 82-96):
```go
mux := http.NewServeMux()
mux.HandleFunc("/", h.Index)
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/preferences", h.Preferences)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))

// Serve embedded static files (CSS, JS, images).
staticFS, err := views.StaticFS()
if err != nil {
    log.Fatalf("failed to load static files: %v", err)
}
var subFS fs.FS = staticFS
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
```

New code:
```go
mux := http.NewServeMux()

// API routes - handled by Go
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))

// SPA handler - serves React app for all other routes
spaHandler := spa.NewHandler()
mux.Handle("/", spaHandler)
```

- [ ] **Step 2: Add spa import**

Add to imports (after `"github.com/metamorphosis-dev/kafka/internal/search"`):
```go
"github.com/metamorphosis-dev/kafka/internal/spa"
```

- [ ] **Step 3: Remove unused views import if needed**

If `views` is only used for `StaticFS()`, remove the import. The template rendering functions (`RenderIndex`, etc.) won't be needed anymore.

- [ ] **Step 4: Verify build**

```bash
cd /home/ashie/git/kafka && go build ./cmd/kafka/
```

Expected: Builds successfully (may fail on embed if dist not found - continue to next task)

---

## Task 4: Wire React to Real API

**Files:**
- Modify: `src/hooks/use-search.ts` in `/tmp/search-zen-50/`

- [ ] **Step 1: Replace mock search with real API call**

Replace the `search` function in `use-search.ts`:

Old code (lines 23-36):
```typescript
const search = useCallback(async (query: string) => {
  if (!query.trim()) return;

  setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));

  // Simulate network delay
  await new Promise((r) => setTimeout(r, 800));

  setState((prev) => ({
    ...prev,
    isLoading: false,
    results: { ...MOCK_RESPONSE, query },
  }));
}, []);
```

New code:
```typescript
const search = useCallback(async (query: string) => {
  if (!query.trim()) return;

  setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));

  try {
    const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    setState((prev) => ({
      ...prev,
      isLoading: false,
      results: data,
    }));
  } catch (err) {
    setState((prev) => ({
      ...prev,
      isLoading: false,
      error: err instanceof Error ? err.message : "Search failed",
    }));
  }
}, []);
```

- [ ] **Step 2: Remove mock data import**

Remove the mock import line (should be near line 2):
```typescript
import { MOCK_RESPONSE, type SearXNGResponse, type Category } from "@/lib/mock-data";
```

Replace with:
```typescript
import type { SearXNGResponse, Category } from "@/lib/mock-data";
```

- [ ] **Step 3: Keep the CATEGORIES export**

Ensure `mock-data.ts` still exports `CATEGORIES` and `Category` type. The file should look like:

```typescript
// Keep these exports - used by CategoryTabs and preferences
export const CATEGORIES = ["general", "it", "images", "news"] as const;
export type Category = typeof CATEGORIES[number];

// Keep interfaces
export interface SearchResult {
  url: string;
  title: string;
  content: string;
  engine: string;
  parsed_url: [string, string, string, string, string];
  engines: string[];
  positions: number[];
  score: number;
  category: string;
  pretty_url: string;
  img_src?: string;
  thumbnail?: string;
  publishedDate?: string;
}

export interface SearXNGResponse {
  query: string;
  number_of_results: number;
  results: SearchResult[];
  answers: string[];
  corrections: string[];
  infoboxes: any[];
  suggestions: string[];
  unresponsive_engines: string[];
}
```

---

## Task 5: Rebuild React and Verify

**Files:**
- Build: `/tmp/search-zen-50/dist/`

- [ ] **Step 1: Rebuild with changes**

```bash
cd /tmp/search-zen-50 && bun run build
```

- [ ] **Step 2: Copy dist to kafka**

```bash
rm -rf /home/ashie/git/kafka/internal/spa/dist
cp -r /tmp/search-zen-50/dist /home/ashie/git/kafka/internal/spa/dist
```

- [ ] **Step 3: Verify Go build**

```bash
cd /home/ashie/git/kafka && go build ./cmd/kafka/ && echo "Build successful"
```

Expected: "Build successful"

---

## Task 6: Test the Integration

- [ ] **Step 1: Start the server**

```bash
cd /home/ashie/git/kafka && ./kafka -config config.toml &
sleep 2
```

- [ ] **Step 2: Test homepage**

```bash
curl -s http://localhost:8080/ | head -20
```

Expected: HTML with `<div id="root"></div>` from React app

- [ ] **Step 3: Test API**

```bash
curl -s "http://localhost:8080/search?format=json&q=test" | head -50
```

Expected: JSON search response

- [ ] **Step 4: Clean up**

```bash
pkill -f "./kafka" 2>/dev/null; echo "Done"
```

---

## Dependencies

- Node.js/Bun for building React app
- Go 1.24+ for embed functionality
- No new Go dependencies

## Notes

- The `internal/spa/dist/` folder should be gitignored (build artifact)
- The `internal/spa/dist/` copy is needed for the embed to work at compile time
- Preferences page is entirely client-side (localStorage) - no backend needed
- Autocomplete can be added later by modifying `SearchInput.tsx` to call `/autocompleter`
