/**
 * NodeBench MCP — Local Dashboard Server
 *
 * Serves a local web dashboard showing the full UI Dive flywheel cycle:
 * explored routes, components, interactions, screenshots, bugs,
 * code locations, fixes, changelogs, generated tests, code reviews.
 *
 * Inspired by Serena MCP's local dashboard. Zero external deps — uses
 * Node's built-in http module + reads from existing SQLite DB.
 */
import { createServer } from "node:http";
import { getDashboardHtml } from "./html.js";
let _server = null;
let _port = 0;
/** Start the dashboard HTTP server. Returns the port it's listening on. */
export function startDashboardServer(db, preferredPort = 6274) {
    return new Promise((resolve, reject) => {
        if (_server) {
            resolve(_port);
            return;
        }
        _server = createServer((req, res) => handleRequest(db, req, res));
        let attempts = 0;
        const maxAttempts = 10;
        function tryListen(port) {
            _server.listen(port, "127.0.0.1", () => {
                _port = port;
                resolve(_port);
            });
        }
        _server.on("error", (err) => {
            if (err.code === "EADDRINUSE" && attempts < maxAttempts) {
                attempts++;
                tryListen(preferredPort + attempts);
            }
            else {
                reject(err);
            }
        });
        tryListen(preferredPort);
    });
}
/** Stop the dashboard server */
export function stopDashboardServer() {
    if (_server) {
        _server.close();
        _server = null;
        _port = 0;
    }
}
/** Get the current dashboard URL */
export function getDashboardUrl() {
    return _port ? `http://127.0.0.1:${_port}` : null;
}
// ── Request Router ──────────────────────────────────────────────────
function handleRequest(db, req, res) {
    const url = new URL(req.url || "/", `http://127.0.0.1:${_port}`);
    const path = url.pathname;
    // CORS for local dev
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
    if (req.method === "OPTIONS") {
        res.writeHead(204);
        res.end();
        return;
    }
    try {
        if (path === "/" || path === "/index.html") {
            serveHtml(res);
        }
        else if (path === "/api/sessions") {
            apiSessions(db, res);
        }
        else if (path.startsWith("/api/screenshot/") && path.endsWith("/image")) {
            const ssId = decodeURIComponent(path.split("/api/screenshot/")[1].split("/image")[0]);
            apiScreenshotImage(db, ssId, res);
        }
        else if (path.startsWith("/api/session/")) {
            const sessionId = decodeURIComponent(path.split("/api/session/")[1].split("/")[0]);
            const sub = path.split(sessionId + "/")[1] || "overview";
            apiSessionDetail(db, sessionId, sub, res);
        }
        else if (path === "/api/latest") {
            apiLatestSession(db, res);
        }
        else if (path === "/api/agents/status") {
            apiAgentStatus(db, res);
        }
        else if (path === "/api/agents/activity") {
            apiAgentActivity(db, res);
        }
        else if (path === "/api/agents/mailbox") {
            apiAgentMailbox(db, res);
        }
        else {
            res.writeHead(404, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "Not found" }));
        }
    }
    catch (err) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: err.message }));
    }
}
// ── HTML ────────────────────────────────────────────────────────────
function serveHtml(res) {
    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(getDashboardHtml());
}
// ── API: List sessions ──────────────────────────────────────────────
function apiSessions(db, res) {
    const rows = db.prepare(`
    SELECT s.id, s.app_url, s.app_name, s.status, s.created_at, s.completed_at,
      (SELECT COUNT(*) FROM ui_dive_components WHERE session_id = s.id) as component_count,
      (SELECT COUNT(*) FROM ui_dive_bugs WHERE session_id = s.id) as bug_count,
      (SELECT COUNT(*) FROM ui_dive_bugs WHERE session_id = s.id AND status = 'resolved') as bugs_resolved,
      (SELECT COUNT(*) FROM ui_dive_interactions WHERE session_id = s.id) as interaction_count,
      (SELECT COUNT(*) FROM ui_dive_fix_verifications WHERE session_id = s.id) as fix_count,
      (SELECT COUNT(*) FROM ui_dive_code_reviews WHERE session_id = s.id) as review_count
    FROM ui_dive_sessions s
    ORDER BY s.created_at DESC
    LIMIT 50
  `).all();
    json(res, rows);
}
// ── API: Latest session redirect ────────────────────────────────────
function apiLatestSession(db, res) {
    const row = db.prepare(`
    SELECT id FROM ui_dive_sessions ORDER BY created_at DESC LIMIT 1
  `).get();
    if (!row) {
        json(res, { error: "No sessions found" }, 404);
        return;
    }
    // Return full overview for latest
    apiSessionDetail(db, row.id, "overview", res);
}
// ── API: Session detail ─────────────────────────────────────────────
function apiSessionDetail(db, sessionId, sub, res) {
    switch (sub) {
        case "overview":
            return apiOverview(db, sessionId, res);
        case "components":
            return apiComponents(db, sessionId, res);
        case "bugs":
            return apiBugs(db, sessionId, res);
        case "interactions":
            return apiInteractions(db, sessionId, res);
        case "screenshots":
            return apiScreenshots(db, sessionId, res);
        case "code-locations":
            return apiCodeLocations(db, sessionId, res);
        case "fixes":
            return apiFixes(db, sessionId, res);
        case "changelogs":
            return apiChangelogs(db, sessionId, res);
        case "tests":
            return apiTests(db, sessionId, res);
        case "reviews":
            return apiReviews(db, sessionId, res);
        case "design-issues":
            return apiDesignIssues(db, sessionId, res);
        default:
            json(res, { error: `Unknown sub-route: ${sub}` }, 404);
    }
}
function apiOverview(db, sid, res) {
    const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sid);
    if (!session) {
        json(res, { error: "Session not found" }, 404);
        return;
    }
    const stats = {
        components: db.prepare("SELECT COUNT(*) as c FROM ui_dive_components WHERE session_id = ?").get(sid),
        interactions: db.prepare("SELECT COUNT(*) as c FROM ui_dive_interactions WHERE session_id = ?").get(sid),
        bugs: db.prepare("SELECT COUNT(*) as c FROM ui_dive_bugs WHERE session_id = ?").get(sid),
        bugsOpen: db.prepare("SELECT COUNT(*) as c FROM ui_dive_bugs WHERE session_id = ? AND status = 'open'").get(sid),
        bugsResolved: db.prepare("SELECT COUNT(*) as c FROM ui_dive_bugs WHERE session_id = ? AND status = 'resolved'").get(sid),
        screenshots: db.prepare("SELECT COUNT(*) as c FROM ui_dive_screenshots WHERE session_id = ?").get(sid),
        tests: db.prepare("SELECT COUNT(*) as c FROM ui_dive_interaction_tests WHERE session_id = ?").get(sid),
        designIssues: db.prepare("SELECT COUNT(*) as c FROM ui_dive_design_issues WHERE session_id = ?").get(sid),
        codeLocations: db.prepare("SELECT COUNT(*) as c FROM ui_dive_code_locations WHERE session_id = ?").get(sid),
        fixes: db.prepare("SELECT COUNT(*) as c FROM ui_dive_fix_verifications WHERE session_id = ?").get(sid),
        fixesVerified: db.prepare("SELECT COUNT(*) as c FROM ui_dive_fix_verifications WHERE session_id = ? AND verified = 1").get(sid),
        changelogs: db.prepare("SELECT COUNT(*) as c FROM ui_dive_changelogs WHERE session_id = ?").get(sid),
        generatedTests: db.prepare("SELECT COUNT(*) as c FROM ui_dive_generated_tests WHERE session_id = ?").get(sid),
        codeReviews: db.prepare("SELECT COUNT(*) as c FROM ui_dive_code_reviews WHERE session_id = ?").get(sid),
    };
    const latestReview = db.prepare("SELECT score, summary, severity_counts, recommendations FROM ui_dive_code_reviews WHERE session_id = ? ORDER BY created_at DESC LIMIT 1").get(sid);
    json(res, {
        session,
        stats: Object.fromEntries(Object.entries(stats).map(([k, v]) => [k, v?.c ?? 0])),
        latestReview,
    });
}
function apiComponents(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ? ORDER BY created_at").all(sid));
}
function apiBugs(db, sid, res) {
    json(res, db.prepare(`
    SELECT b.*, c.name as component_name
    FROM ui_dive_bugs b
    LEFT JOIN ui_dive_components c ON b.component_id = c.id
    WHERE b.session_id = ?
    ORDER BY
      CASE b.severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
      b.created_at
  `).all(sid));
}
function apiInteractions(db, sid, res) {
    json(res, db.prepare(`
    SELECT i.*, c.name as component_name
    FROM ui_dive_interactions i
    LEFT JOIN ui_dive_components c ON i.component_id = c.id
    WHERE i.session_id = ?
    ORDER BY i.sequence_num
  `).all(sid));
}
function apiScreenshots(db, sid, res) {
    json(res, db.prepare("SELECT id, session_id, component_id, label, route, file_path, base64_thumbnail, width, height, metadata, created_at FROM ui_dive_screenshots WHERE session_id = ? ORDER BY created_at").all(sid));
}
/** Serve a screenshot as an actual PNG image */
function apiScreenshotImage(db, ssId, res) {
    const row = db.prepare("SELECT base64_thumbnail, file_path FROM ui_dive_screenshots WHERE id = ?").get(ssId);
    if (!row) {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("Screenshot not found");
        return;
    }
    // Prefer inline base64, fall back to file on disk
    if (row.base64_thumbnail) {
        const buf = Buffer.from(row.base64_thumbnail, "base64");
        res.writeHead(200, {
            "Content-Type": "image/png",
            "Content-Length": buf.length.toString(),
            "Cache-Control": "public, max-age=86400",
        });
        res.end(buf);
        return;
    }
    if (row.file_path) {
        try {
            const fs = require("node:fs");
            const buf = fs.readFileSync(row.file_path);
            res.writeHead(200, {
                "Content-Type": "image/png",
                "Content-Length": buf.length.toString(),
                "Cache-Control": "public, max-age=86400",
            });
            res.end(buf);
        }
        catch {
            res.writeHead(404, { "Content-Type": "text/plain" });
            res.end("Screenshot file not found on disk");
        }
        return;
    }
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end("No image data available");
}
function apiCodeLocations(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_code_locations WHERE session_id = ? ORDER BY created_at").all(sid));
}
function apiFixes(db, sid, res) {
    json(res, db.prepare(`
    SELECT f.*, b.title as bug_title, b.severity as bug_severity, b.status as bug_status
    FROM ui_dive_fix_verifications f
    LEFT JOIN ui_dive_bugs b ON f.bug_id = b.id
    WHERE f.session_id = ?
    ORDER BY f.created_at
  `).all(sid));
}
function apiChangelogs(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_changelogs WHERE session_id = ? ORDER BY created_at DESC").all(sid));
}
function apiTests(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_generated_tests WHERE session_id = ? ORDER BY created_at").all(sid));
}
function apiReviews(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_code_reviews WHERE session_id = ? ORDER BY created_at DESC").all(sid));
}
function apiDesignIssues(db, sid, res) {
    json(res, db.prepare("SELECT * FROM ui_dive_design_issues WHERE session_id = ? ORDER BY created_at").all(sid));
}
// ── API: Agent Monitor ──────────────────────────────────────────────
function apiAgentStatus(db, res) {
    // Only agents active in the last 2 hours (has recent calls OR a claimed task)
    const roles = db.prepare(`
    SELECT r.session_id, r.role, r.focus_area, r.created_at
    FROM agent_roles r
    WHERE EXISTS (
      SELECT 1 FROM tool_call_log t
      WHERE t.session_id = r.session_id AND t.created_at >= datetime('now', '-2 hours')
    ) OR EXISTS (
      SELECT 1 FROM agent_tasks a
      WHERE a.session_id = r.session_id AND a.status = 'claimed'
        AND a.claimed_at >= datetime('now', '-4 hours')
    )
    ORDER BY r.created_at DESC
    LIMIT 20
  `).all();
    const agents = roles.map(r => {
        const task = db.prepare(`
      SELECT task_key, description, claimed_at
      FROM agent_tasks
      WHERE session_id = ? AND status = 'claimed'
      ORDER BY claimed_at DESC LIMIT 1
    `).get(r.session_id);
        const callCount = db.prepare(`
      SELECT COUNT(*) as c FROM tool_call_log
      WHERE session_id = ? AND created_at >= datetime('now', '-30 minutes')
    `).get(r.session_id);
        const lastCall = db.prepare(`
      SELECT created_at FROM tool_call_log
      WHERE session_id = ? ORDER BY created_at DESC LIMIT 1
    `).get(r.session_id);
        const budget = db.prepare(`
      SELECT tokens_used, tokens_limit FROM context_budget_log
      WHERE session_id = ? ORDER BY created_at DESC LIMIT 1
    `).get(r.session_id);
        const unread = db.prepare(`
      SELECT COUNT(*) as c FROM agent_mailbox
      WHERE (recipient_id = ? OR recipient_role = ?) AND read = 0
    `).get(r.session_id, r.role);
        return {
            sessionId: r.session_id,
            role: r.role,
            focusArea: r.focus_area,
            assignedAt: r.created_at,
            currentTask: task ? { key: task.task_key, description: task.description, claimedAt: task.claimed_at } : null,
            toolCallCount: callCount.c,
            lastCallAt: lastCall?.created_at ?? null,
            tokenBudget: budget
                ? { used: budget.tokens_used, limit: budget.tokens_limit, percent: Math.round((budget.tokens_used / budget.tokens_limit) * 100) }
                : { used: 0, limit: 200000, percent: 0 },
            unreadMessages: unread.c,
        };
    });
    // Summary stats
    const activeSessions = agents.map(a => a.sessionId);
    const totalCalls = activeSessions.length > 0
        ? db.prepare(`SELECT COUNT(*) as c FROM tool_call_log WHERE session_id IN (${activeSessions.map(() => "?").join(",")})`)
            .get(...activeSessions).c
        : 0;
    const claimedCount = agents.filter(a => a.currentTask).length;
    const completedTasks = db.prepare(`
    SELECT task_key, session_id, description, progress_note, released_at
    FROM agent_tasks
    WHERE status = 'released' AND released_at >= datetime('now', '-1 hour')
    ORDER BY released_at DESC LIMIT 20
  `).all();
    json(res, {
        agents,
        completedTasks,
        summary: {
            activeAgents: agents.length,
            activeTasks: claimedCount,
            completedTasks: completedTasks.length,
            totalCalls,
        },
    });
}
function apiAgentActivity(db, res) {
    // Only show calls from agents active in last 2 hours
    const calls = db.prepare(`
    SELECT t.tool_name, t.session_id, t.result_status, t.duration_ms, t.created_at
    FROM tool_call_log t
    WHERE EXISTS (
      SELECT 1 FROM agent_roles r
      WHERE r.session_id = t.session_id AND (
        EXISTS (SELECT 1 FROM tool_call_log t2 WHERE t2.session_id = r.session_id AND t2.created_at >= datetime('now', '-2 hours'))
        OR EXISTS (SELECT 1 FROM agent_tasks a WHERE a.session_id = r.session_id AND a.status = 'claimed')
      )
    )
    ORDER BY t.created_at DESC LIMIT 30
  `).all();
    json(res, { calls, totalCalls: calls.length });
}
function apiAgentMailbox(db, res) {
    // Deduplicate by subject+sender — keep only the latest of each
    const messages = db.prepare(`
    SELECT id, sender_id, recipient_id, recipient_role, category, priority, subject, body, created_at
    FROM agent_mailbox
    WHERE read = 0 AND id IN (
      SELECT id FROM (
        SELECT id, ROW_NUMBER() OVER (PARTITION BY sender_id, subject ORDER BY created_at DESC) as rn
        FROM agent_mailbox WHERE read = 0
      ) WHERE rn = 1
    )
    ORDER BY
      CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
      created_at DESC
    LIMIT 10
  `).all();
    json(res, { messages });
}
// ── Helpers ─────────────────────────────────────────────────────────
function json(res, data, status = 200) {
    res.writeHead(status, { "Content-Type": "application/json" });
    res.end(JSON.stringify(data));
}
//# sourceMappingURL=server.js.map