# Admin Tool — Dealer Data Rebuild — 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:** Build `admin.js` — a single-file Node.js local server with a browser UI that rebuilds and re-embeds dealer data into `index.html` from the CSV in one button press.

**Architecture:** A Node.js `http` server (no npm) serves inline HTML at `GET /`, returns current status JSON at `GET /status`, and executes the full rebuild pipeline at `POST /rebuild`. The rebuild spawns `node build-data.js`, reads the resulting `dealers-data.js`, replaces the DEALERS embed in `index.html`, then deletes the intermediate file.

**Tech Stack:** Node.js built-ins only — `http`, `child_process`, `fs`, `path`; all UI HTML/CSS/JS inlined as a template string.

---

## File Structure

- **Create:** `admin.js` — the entire admin tool (server + inline UI)

No other files are created or modified.

---

### Task 1: HTTP server skeleton + `/` route serving inline HTML

**Files:**
- Create: `admin.js`

- [ ] **Step 1: Create `admin.js` with server skeleton**

```javascript
// admin.js — Dealer data admin tool
// Usage: node admin.js
// Opens browser UI at http://localhost:3000
'use strict';
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const DIR = __dirname;
const PROJECT_NAME = path.basename(DIR);

// ── Helpers ──────────────────────────────────────────────────────────────────

function findCSV() {
  const files = fs.readdirSync(DIR).filter(f => f.endsWith('.csv'));
  return files.length ? files[0] : null;
}

function countDealersInHTML() {
  const htmlPath = path.join(DIR, 'index.html');
  if (!fs.existsSync(htmlPath)) return null;
  const html = fs.readFileSync(htmlPath, 'utf8');
  const start = html.indexOf('  <script>\nconst DEALERS = [');
  const end = html.indexOf('];\n  </script>', start);
  if (start === -1 || end === -1) return null;
  const arr = html.slice(start + '  <script>\nconst DEALERS = '.length, end + 1);
  try {
    return JSON.parse(arr).length;
  } catch {
    return null;
  }
}

function getHTMLMtime() {
  const htmlPath = path.join(DIR, 'index.html');
  if (!fs.existsSync(htmlPath)) return null;
  return fs.statSync(htmlPath).mtime.toISOString();
}
```

- [ ] **Step 2: Add inline UI HTML constant**

Append to `admin.js`:

```javascript
// ── Inline UI ─────────────────────────────────────────────────────────────────

const HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${PROJECT_NAME} — Admin</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial,Helvetica,sans-serif;background:#f5f5f5;color:#111;display:flex;justify-content:center;padding:40px 16px}
.card{background:#fff;border:1px solid #ddd;border-radius:8px;padding:32px 36px;max-width:520px;width:100%}
h1{font-size:18px;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}
h2{font-size:12px;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:28px}
.status-grid{display:grid;grid-template-columns:auto 1fr;gap:6px 12px;margin-bottom:28px;font-size:13px}
.label{color:#888}
.value{font-weight:600}
.ok{color:#1a7a2e}
.warn{color:#c0392b}
button{background:#000;color:#fff;border:none;border-radius:4px;padding:10px 22px;font-size:13px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;cursor:pointer;display:inline-flex;align-items:center;gap:8px}
button:disabled{background:#999;cursor:not-allowed}
button:hover:not(:disabled){background:#BA0C2F}
.log{margin-top:20px;background:#111;color:#eee;border-radius:4px;padding:14px 16px;font-size:12px;font-family:monospace;min-height:80px;max-height:220px;overflow-y:auto;white-space:pre-wrap;line-height:1.6}
.log-ok{color:#4ec94e}
.log-err{color:#e74c3c}
.spinner{width:14px;height:14px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;animation:spin .6s linear infinite;display:none}
button.busy .spinner{display:block}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="card">
  <h1 id="projectName"></h1>
  <h2>Admin — Dealer Data</h2>
  <div class="status-grid" id="statusGrid">
    <span class="label">CSV</span><span class="value" id="csvStatus">—</span>
    <span class="label">Last rebuilt</span><span class="value" id="lastRebuilt">—</span>
    <span class="label">Dealers in index.html</span><span class="value" id="dealerCount">—</span>
  </div>
  <button id="rebuildBtn" onclick="rebuild()">
    <div class="spinner"></div>
    Rebuild from CSV
  </button>
  <div class="log" id="log"></div>
</div>
<script>
const $ = id => document.getElementById(id);
function log(msg, cls) {
  const el = $('log');
  const line = document.createElement('div');
  if (cls) line.className = cls;
  line.textContent = '> ' + msg;
  el.appendChild(line);
  el.scrollTop = el.scrollHeight;
}
function loadStatus() {
  fetch('/status').then(r => r.json()).then(s => {
    $('projectName').textContent = s.projectName;
    $('csvStatus').innerHTML = s.csv
      ? '<span class="ok">' + s.csv + ' ✓</span>'
      : '<span class="warn">No CSV found</span>';
    $('lastRebuilt').textContent = s.lastRebuilt
      ? new Date(s.lastRebuilt).toLocaleString()
      : 'Never';
    $('dealerCount').textContent = s.dealerCount !== null ? s.dealerCount : '—';
    $('rebuildBtn').disabled = !s.csv || !s.htmlExists;
    if (!s.htmlExists) $('rebuildBtn').title = 'index.html not found';
  });
}
function rebuild() {
  const btn = $('rebuildBtn');
  btn.disabled = true;
  btn.classList.add('busy');
  $('log').innerHTML = '';
  log('Starting rebuild...');
  fetch('/rebuild', { method: 'POST' })
    .then(r => r.json())
    .then(res => {
      if (res.ok) {
        log('build-data.js completed', '');
        log(res.count + ' dealers written', '');
        log('index.html updated', '');
        log('Done — ' + res.count + ' dealers in index.html', 'log-ok');
      } else {
        log('ERROR: ' + res.error, 'log-err');
      }
      loadStatus();
    })
    .catch(err => log('ERROR: ' + err.message, 'log-err'))
    .finally(() => { btn.disabled = false; btn.classList.remove('busy'); });
}
loadStatus();
</script>
</body>
</html>`;
```

- [ ] **Step 3: Add `/status` route and request router**

Append to `admin.js`:

```javascript
// ── Routes ────────────────────────────────────────────────────────────────────

function handleStatus(res) {
  const csv = findCSV();
  const status = {
    projectName: PROJECT_NAME,
    csv: csv || null,
    htmlExists: fs.existsSync(path.join(DIR, 'index.html')),
    lastRebuilt: getHTMLMtime(),
    dealerCount: countDealersInHTML()
  };
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(status));
}

function handleRebuild(res) {
  const buildScript = path.join(DIR, 'build-data.js');
  if (!fs.existsSync(buildScript)) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: false, error: 'build-data.js not found in project directory' }));
    return;
  }

  execFile(process.execPath, [buildScript], { cwd: DIR }, (err, stdout, stderr) => {
    if (err) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: false, error: stderr || err.message }));
      return;
    }

    // Read dealers-data.js
    const dataPath = path.join(DIR, 'dealers-data.js');
    if (!fs.existsSync(dataPath)) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: false, error: 'dealers-data.js was not created by build-data.js' }));
      return;
    }
    const dataContent = fs.readFileSync(dataPath, 'utf8');

    // Extract JSON array from "const DEALERS = [...];\n"
    const arrStart = dataContent.indexOf('[');
    const arrEnd = dataContent.lastIndexOf(']');
    if (arrStart === -1 || arrEnd === -1) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: false, error: 'Could not parse dealers-data.js' }));
      return;
    }
    const dealersJSON = dataContent.slice(arrStart, arrEnd + 1);

    // Count dealers
    let count = 0;
    try { count = JSON.parse(dealersJSON).length; } catch {}

    // Embed into index.html
    const htmlPath = path.join(DIR, 'index.html');
    if (!fs.existsSync(htmlPath)) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: false, error: 'index.html not found' }));
      return;
    }
    const html = fs.readFileSync(htmlPath, 'utf8');
    const START_MARKER = '  <script>\nconst DEALERS = [';
    const END_MARKER = '];\n  </script>';
    const startIdx = html.indexOf(START_MARKER);
    const endIdx = html.indexOf(END_MARKER, startIdx);
    if (startIdx === -1 || endIdx === -1) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: false, error: 'Embed markers not found in index.html' }));
      return;
    }
    const newHTML =
      html.slice(0, startIdx + START_MARKER.length) +
      dealersJSON.slice(1, -1) +  // strip outer [ and ]
      html.slice(endIdx);
    fs.writeFileSync(htmlPath, newHTML, 'utf8');

    // Clean up intermediate file
    try { fs.unlinkSync(dataPath); } catch {}

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, count }));
  });
}

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(HTML);
  } else if (req.method === 'GET' && req.url === '/status') {
    handleStatus(res);
  } else if (req.method === 'POST' && req.url === '/rebuild') {
    handleRebuild(res);
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

server.on('error', err => {
  if (err.code === 'EADDRINUSE') {
    console.error(`Port ${PORT} in use — kill the process or set PORT env var: PORT=3001 node admin.js`);
  } else {
    console.error(err);
  }
  process.exit(1);
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`Admin server running at http://localhost:${PORT}`);
  console.log(`Project: ${PROJECT_NAME}`);
  console.log('Press Ctrl+C to stop');
});
```

- [ ] **Step 4: Verify the file looks correct**

```bash
node -e "require('./admin.js')" &
sleep 1
curl -s http://localhost:3000/status | node -e "process.stdin.resume();process.stdin.on('data',d=>console.log(JSON.parse(d)))"
kill %1
```

Expected: JSON object with `projectName`, `csv`, `htmlExists: true`, `dealerCount` showing current count.

- [ ] **Step 5: Commit**

```bash
git add admin.js
git commit -m "feat: add admin.js — browser UI for rebuilding dealer data from CSV"
```

---

### Task 2: Manual end-to-end smoke test

**Files:**
- No changes — test only

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

```bash
node admin.js
```

Expected output:
```
Admin server running at http://localhost:3000
Project: Jeep-dealer-locator
Press Ctrl+C to stop
```

- [ ] **Step 2: Open browser at http://localhost:3000**

Verify:
- Title shows "JEEP-DEALER-LOCATOR"
- CSV row shows filename with green tick (e.g. `Jeep_south_Africa_Dealer.csv ✓`)
- Last rebuilt shows a date/time
- Dealers in index.html shows a number (e.g. 33)
- "Rebuild from CSV" button is enabled

- [ ] **Step 3: Click "Rebuild from CSV"**

Verify:
- Button shows spinner briefly while rebuilding
- Log area shows:
  ```
  > Starting rebuild...
  > build-data.js completed
  > 33 dealers written
  > index.html updated
  > Done — 33 dealers in index.html
  ```
  (green final line)
- Status row updates with new "Last rebuilt" timestamp
- Button re-enables

- [ ] **Step 4: Confirm index.html was updated**

```bash
node -e "
const fs = require('fs');
const h = fs.readFileSync('index.html', 'utf8');
const s = h.indexOf('const DEALERS = [');
const e = h.indexOf('];', s);
const arr = JSON.parse(h.slice(s + 'const DEALERS = '.length, e + 1));
console.log('Dealers in index.html:', arr.length);
"
```

Expected: `Dealers in index.html: 33` (or current count)

- [ ] **Step 5: Stop the server**

Press `Ctrl+C` in the terminal running `node admin.js`.
