/**
 * Mind Palace Dashboard — HTTP Server (v2.0 — Step 8)
 *
 * Zero-dependency HTTP server serving the Prism Mind Palace UI.
 * Runs alongside the MCP stdio server on a separate port.
 *
 * ═══════════════════════════════════════════════════════════════════
 * CRITICAL MCP SAFETY:
 *   The MCP server communicates via stdout. ANY console.log() here
 *   will corrupt the JSON-RPC stream and crash the agent.
 *   All logging uses console.error() exclusively.
 *
 * ENDPOINTS:
 *   GET /                   → Dashboard UI (HTML)
 *   GET /api/projects       → List all projects with handoff data
 *   GET /api/project?name=  → Full project data (context, ledger, history)
 * ═══════════════════════════════════════════════════════════════════
 */

import * as http from "http";
import * as path from "path";
import * as os from "os";
import * as fs from "fs";

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createServer, getAllPossibleTools } from "../server.js";
import { getStorage } from "../storage/index.js";
import { PRISM_USER_ID, SERVER_CONFIG } from "../config.js";
import { renderDashboardHTML } from "./ui.js";
import { computeIntentHealth } from "./intentHealth.js";
import { getAllSettings, setSetting, getSetting, getSettingSync } from "../storage/configStorage.js";
import { compactLedgerHandler } from "../tools/compactionHandler.js";
import { getLLMProvider } from "../utils/llm/factory.js";
import { buildVaultDirectory } from "../utils/vaultExporter.js";
import { redactSettings } from "../tools/commonHelpers.js";
import { handleGraphRoutes } from "./graphRouter.js";
import {
  safeCompare,
  generateToken,
  isAuthenticated,
  createRateLimiter,
  initJWKS,
  type AuthConfig,
  type PrismAuthenticatedRequest,
} from "./authUtils.js";


const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);

/** Read HTTP request body as string (Buffer-based to avoid GC thrash on large imports) */
/** SECURITY: 10MB limit prevents memory exhaustion from oversized POST payloads. */
const MAX_BODY_BYTES = parseInt(process.env.PRISM_MAX_REQUEST_BYTES ?? String(10 * 1024 * 1024), 10);
function readBody(req: http.IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    let totalBytes = 0;
    req.on("data", (chunk: Buffer) => {
      totalBytes += chunk.length;
      if (totalBytes > MAX_BODY_BYTES) {
        req.destroy(new Error("Request body too large (>10MB)"));
        return reject(new Error("Request body too large"));
      }
      chunks.push(chunk);
    });
    req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
    req.on("error", reject);
  });
}

export async function startDashboardServer(): Promise<void> {
  // Port 3000 conflicts are gracefully handled by server.ts catching EADDRINUSE
  // which will just disable the dashboard on secondary instances, keeping MCP alive.

  // Lazy storage accessor — returns null if storage isn't ready yet.
  // API routes gracefully degrade with 503 instead of blocking startup.
  let _storage: Awaited<ReturnType<typeof getStorage>> | null = null;
  const getStorageSafe = async (): Promise<Awaited<ReturnType<typeof getStorage>> | null> => {
    if (_storage) return _storage;
    try {
      _storage = await getStorage();
      return _storage;
    } catch {
      return null;
    }
  };

  /**
   * v5.1: Optional HTTP Basic Auth for remote dashboard access.
   *
   * HOW IT WORKS:
   *   1. If PRISM_DASHBOARD_USER and PRISM_DASHBOARD_PASS are NOT set → auth is disabled (backward compatible)
   *   2. If set → every request must provide Basic Auth credentials OR a valid session cookie
   *   3. On successful auth → a session cookie (24h) is set so users don't re-authenticate on every request
   *   4. On failure → a styled login page is shown (not a raw 401 popup)
   *
   * SECURITY NOTES:
   *   - This is HTTP Basic Auth — suitable for LAN/VPN access, NOT public internet without HTTPS
   *   - Session tokens are random 64-char hex strings stored in-memory (cleared on server restart)
   *   - Timing-safe comparison prevents credential timing attacks
   */
  const AUTH_USER = process.env.PRISM_DASHBOARD_USER || "";
  const AUTH_PASS = process.env.PRISM_DASHBOARD_PASS || "";
  const AUTH_JWKS_URI = process.env.PRISM_JWKS_URI || process.env.AUTH_JWKS_URI || "";

  // Auth is enabled if basic auth is configured OR if JWKS is configured
  const AUTH_ENABLED = (AUTH_USER.length > 0 && AUTH_PASS.length > 0) || AUTH_JWKS_URI.length > 0;

  if (AUTH_JWKS_URI) {
    initJWKS(AUTH_JWKS_URI);
  }

  const SESSION_TTL_MS = parseInt(process.env.PRISM_SESSION_TTL_MS ?? String(24 * 60 * 60 * 1000), 10);
  const activeSessions = new Map<string, number>();

  // Auth config object — injectable for testing via authUtils.ts
  const authConfig: AuthConfig = {
    authEnabled: AUTH_ENABLED,
    authUser: AUTH_USER,
    authPass: AUTH_PASS,
    activeSessions,
    jwtAudience: process.env.PRISM_JWT_AUDIENCE || undefined,
    jwtIssuer: process.env.PRISM_JWT_ISSUER || undefined,
  };

  // v6.5.1: Rate limiter for login endpoint — 5 attempts per 60 seconds per IP
  const loginRateLimiter = createRateLimiter({
    maxAttempts: 5,
    windowMs: 60 * 1000,
  });

  // Track active SSE transports for MCP
  const activeSSETransports = new Map<string, SSEServerTransport>();

  /** Render a styled login page matching the Mind Palace theme */
  function renderLoginPage(): string {
    return `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Prism MCP — Login</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0a0e1a;color:#f1f5f9;font-family:'Inter',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}
.bg{position:fixed;inset:0;background-image:radial-gradient(circle at 20% 30%,rgba(139,92,246,0.08) 0%,transparent 50%),radial-gradient(circle at 80% 70%,rgba(59,130,246,0.06) 0%,transparent 50%)}
.login-card{position:relative;z-index:1;background:rgba(17,24,39,0.6);backdrop-filter:blur(16px);border:1px solid rgba(139,92,246,0.15);border-radius:16px;padding:2.5rem;width:380px;max-width:90vw;text-align:center}
.logo{font-size:1.75rem;font-weight:700;background:linear-gradient(135deg,#8b5cf6,#3b82f6,#06b6d4);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
.subtitle{color:#64748b;font-size:0.85rem;margin-bottom:2rem}
.field{margin-bottom:1rem}
.field input{width:100%;padding:0.7rem 1rem;background:#111827;border:1px solid rgba(139,92,246,0.15);border-radius:10px;color:#f1f5f9;font-size:0.9rem;font-family:'Inter',sans-serif;outline:none;transition:border-color 0.2s}
.field input:focus{border-color:rgba(139,92,246,0.5)}
.field input::placeholder{color:#475569}
.login-btn{width:100%;padding:0.75rem;background:linear-gradient(135deg,#8b5cf6,#3b82f6);color:white;border:none;border-radius:10px;font-size:0.95rem;font-weight:600;cursor:pointer;transition:opacity 0.2s;margin-top:0.5rem}
.login-btn:hover{opacity:0.9}
.error{color:#f43f5e;font-size:0.8rem;margin-top:1rem;display:none}
.lock{font-size:2rem;margin-bottom:1rem}
</style></head><body>
<div class="bg"></div>
<div class="login-card">
<div class="lock">🔒</div>
<div class="logo">🧠 Prism Mind Palace</div>
<div class="subtitle">Authentication required for remote access</div>
<form id="loginForm" onsubmit="return handleLogin(event)">
<div class="field"><input type="text" id="user" placeholder="Username" autocomplete="username" required></div>
<div class="field"><input type="password" id="pass" placeholder="Password" autocomplete="current-password" required></div>
<button type="submit" class="login-btn">Sign In</button>
</form>
<div class="error" id="error">Invalid credentials</div>
</div>
<script>
async function handleLogin(e){e.preventDefault();
var u=document.getElementById('user').value,p=document.getElementById('pass').value;
var r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user:u,pass:p})});
if(r.ok){window.location.reload();}else{document.getElementById('error').style.display='block';}
return false;}
</script></body></html>`;
  }

  if (AUTH_ENABLED) {
    if (AUTH_USER) {
      console.error(`[Dashboard] 🔒 Basic auth enabled for user "${AUTH_USER}"`);
    }
    if (AUTH_JWKS_URI) {
      console.error(`[Dashboard] 🔑 JWKS auth enabled: ${AUTH_JWKS_URI}`);
      if (process.env.PRISM_JWT_AUDIENCE) {
        console.error(`[Dashboard]    Audience: ${process.env.PRISM_JWT_AUDIENCE}`);
      } else {
        console.error(`[Dashboard] ⚠️  No PRISM_JWT_AUDIENCE set — any valid JWT from this JWKS will be accepted.`);
      }
    }
    // Security advisory: HTTP Basic Auth transmits credentials in cleartext.
    // When auth is enabled for remote access, HTTPS (reverse proxy) is strongly recommended.
    console.error(
      `[Dashboard] ⚠️  WARNING: Dashboard uses HTTP (not HTTPS). ` +
      `Credentials are sent in cleartext. Use a reverse proxy (nginx/caddy) ` +
      `with TLS for remote access.`
    );
  }

  const httpServer = http.createServer(async (req, res) => {
    // v6.5.1: CORS — restrict origin when auth is enabled to prevent CSRF
    if (AUTH_ENABLED) {
      const origin = req.headers.origin || "";
      // SECURITY: Allowlist-based CORS — don't echo arbitrary origins with credentials
      const allowedOrigins = new Set([
        `http://localhost:${PORT}`,
        `http://127.0.0.1:${PORT}`,
        process.env.PRISM_DASHBOARD_ORIGIN || "",
      ].filter(Boolean));
      if (origin && allowedOrigins.has(origin)) {
        res.setHeader("Access-Control-Allow-Origin", origin);
        res.setHeader("Access-Control-Allow-Credentials", "true");
      }
    } else {
      res.setHeader("Access-Control-Allow-Origin", `http://localhost:${PORT}`);
    }
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    // SECURITY: Prevent clickjacking via X-Frame-Options and CSP
    res.setHeader("X-Frame-Options", "DENY");
    res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");

    if (req.method === "OPTIONS") {
      res.writeHead(204);
      return res.end();
    }

    // ─── v5.1: Auth login endpoint (always accessible) ───
    const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
    if (AUTH_ENABLED && reqUrl.pathname === "/api/auth/login" && req.method === "POST") {
      // v6.5.1: Rate limiting — prevent brute-force attacks
      const clientIP = (req.socket?.remoteAddress || "unknown").replace(/^::ffff:/, "");
      if (!loginRateLimiter.isAllowed(clientIP)) {
        res.writeHead(429, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ error: "Too many login attempts. Try again later." }));
      }

      const body = await readBody(req);
      try {
        const { user, pass } = JSON.parse(body);
        if (safeCompare(user || "", AUTH_USER) && safeCompare(pass || "", AUTH_PASS)) {
          const token = generateToken();
          activeSessions.set(token, Date.now() + SESSION_TTL_MS);
          // Reset rate limiter on successful login
          loginRateLimiter.reset(clientIP);
          res.writeHead(200, {
            "Content-Type": "application/json",
            "Set-Cookie": `prism_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_TTL_MS / 1000}${(process.env.PRISM_DASHBOARD_ORIGIN?.startsWith("https://") || process.env.PRISM_DASHBOARD_SECURE) ? "; Secure" : ""}`,
          });
          return res.end(JSON.stringify({ ok: true }));
        }
      } catch { /* fall through to 401 */ }
      res.writeHead(401, { "Content-Type": "application/json" });
      return res.end(JSON.stringify({ error: "Invalid credentials" }));
    }

    // ─── v6.5.1: Logout endpoint — invalidates session server-side ───
    if (AUTH_ENABLED && reqUrl.pathname === "/api/auth/logout" && req.method === "POST") {
      // Extract and invalidate the session token from the cookie
      const cookies = req.headers.cookie || "";
      const match = cookies.match(/prism_session=([a-f0-9]{64})/);
      if (match) {
        activeSessions.delete(match[1]);
      }
      res.writeHead(200, {
        "Content-Type": "application/json",
        // Clear the cookie on the client side
        "Set-Cookie": `prism_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0${(process.env.PRISM_DASHBOARD_ORIGIN?.startsWith("https://") || process.env.PRISM_DASHBOARD_SECURE) ? "; Secure" : ""}`,
      });
      return res.end(JSON.stringify({ ok: true }));
    }

    // ─── API: Smithery MCP Server Card (v7.4) ───
    // Smithery explicitly requests this file to skip slow stdio scanning.
    // MUST BE PLACED HERE (above Auth Gate) so it is a fully public manifest.
    if (reqUrl.pathname === "/.well-known/mcp/server-card.json" && req.method === "GET") {
      try {
        const tools = getAllPossibleTools();

        res.writeHead(200, {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*",
          "Cache-Control": "public, max-age=3600"
        });

        return res.end(JSON.stringify({
          $schema: "https://smithery.ai/schema/server-card.json",
          serverInfo: {
            name: SERVER_CONFIG.name,
            version: SERVER_CONFIG.version,
          },
          authentication: {
            required: AUTH_ENABLED
          },
          configSchema: {
            type: "object",
            required: [],
            properties: {
              braveApiKey: {
                type: "string",
                title: "Brave Search API Key",
                description: "API key for Brave Search (powers web search and research tools). Get one at https://brave.com/search/api/"
              },
              googleApiKey: {
                type: "string",
                title: "Google AI API Key",
                description: "API key for Google AI Studio / Gemini (powers synthesis, embeddings, paper analysis). Get one at https://aistudio.google.com/apikey"
              },
              storage: {
                type: "string",
                title: "Storage Backend",
                description: "Storage backend: 'local' (SQLite, zero-config) or 'supabase' (PostgreSQL, multi-device)",
                default: "local",
                enum: ["local", "supabase"]
              },
              supabaseUrl: {
                type: "string",
                title: "Supabase URL",
                description: "Your Supabase project URL (required if storage is 'supabase')"
              },
              supabaseServiceKey: {
                type: "string",
                title: "Supabase Service Key",
                description: "Your Supabase service role key (required if storage is 'supabase')"
              },
              firecrawlApiKey: {
                type: "string",
                title: "Firecrawl API Key",
                description: "Optional: API key for Firecrawl (enables Web Scholar pipeline). Get one at https://www.firecrawl.dev/"
              },
              braveAnswersApiKey: {
                type: "string",
                title: "Brave Answers API Key",
                description: "Optional: Separate key for AI-grounded answers (brave_answers tool)"
              },
              prismEnableHivemind: {
                type: "boolean",
                title: "Enable Multi-Agent Hivemind",
                description: "Set to true to enable multi-agent coordination tools.",
                default: false
              },
              prismDarkFactoryEnabled: {
                type: "boolean",
                title: "Enable Dark Factory",
                description: "Set to true to enable autonomous evaluation pipelines.",
                default: false
              },
              prismTaskRouterEnabled: {
                type: "boolean",
                title: "Enable Task Router",
                description: "Set to true to allow Prism to route tasks to local Ollama agents.",
                default: false
              }
            }
          },
          tools: tools.map(t => ({
            name: t.name,
            description: t.description,
            inputSchema: t.inputSchema
          })),
          prompts: [],
          resources: []
        }));
      } catch (err: any) {
        console.error("[Dashboard] Error generating server card:", err);
        res.writeHead(500, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ error: err.message || "Failed to generate server card" }));
      }
    }

    // ─── AUTHENTICATION GATE ───
    // Basic Auth & Session & JWKS
    if (AUTH_ENABLED && !(await isAuthenticated(req, authConfig))) {
      // If it's an API request, return 401 JSON
      if (reqUrl.pathname.startsWith('/api/') || reqUrl.pathname === "/sse" || reqUrl.pathname === "/messages") {
        res.writeHead(401, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ error: "Authentication required" }));
      }
      // For page requests, show login page
      res.writeHead(401, { "Content-Type": "text/html; charset=utf-8" });
      return res.end(renderLoginPage());
    }

    // ─── AUDIT LOGGING (JWKS / AgentLair) ───
    const authReq = req as PrismAuthenticatedRequest;
    // Log successful agent access (identified via JWKS JWT)
    if (authReq.agent_id && reqUrl.pathname !== "/api/scheduler") {
      let auditLog = `[Dashboard] 🚦 Access: ${req.method} ${reqUrl.pathname} by agent ${authReq.agent_id}`;
      if (authReq.al_name) auditLog += ` (${authReq.al_name})`;
      if (authReq.al_audit_url) auditLog += ` - Audit: ${authReq.al_audit_url}`;
      console.error(auditLog);
    }

    try {
      const url = new URL(req.url || "/", `http://${req.headers.host}`);

      // ─── SSE: MCP Transport Endpoint ───
      if (url.pathname === "/sse" && req.method === "GET") {
        const transport = new SSEServerTransport("/messages", res);
        await transport.start();
        activeSSETransports.set(transport.sessionId, transport);

        transport.onclose = () => {
          activeSSETransports.delete(transport.sessionId);
        };

        try {
          const mcpServer = createServer();
          await mcpServer.connect(transport);
        } catch (err) {
          console.error("[Dashboard] SSE Connection failed:", err);
          activeSSETransports.delete(transport.sessionId);
        }

        return; // SSEServerTransport handles keeping the response open
      }

      // ─── SSE: MCP Message Receiver ───
      if (url.pathname === "/messages" && req.method === "POST") {
        const sessionId = url.searchParams.get("sessionId");
        if (!sessionId) {
          res.writeHead(400, { "Content-Type": "text/plain" });
          return res.end("Missing sessionId");
        }
        const transport = activeSSETransports.get(sessionId);
        if (!transport) {
          res.writeHead(404, { "Content-Type": "text/plain" });
          return res.end("Session not found");
        }
        await transport.handlePostMessage(req, res);
        return;
      }


      // ─── Serve the Dashboard UI ───
      if (url.pathname === "/" || url.pathname === "/index.html") {
        res.writeHead(200, {
          "Content-Type": "text/html; charset=utf-8",
          "Cache-Control": "no-store, no-cache, must-revalidate",
        });
        return res.end(renderDashboardHTML(SERVER_CONFIG.version));
      }

      // ─── API: List all projects ───
      if (url.pathname === "/api/projects") {
        const s = await getStorageSafe();
        if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
        const projects = await s.listProjects();
        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ projects }));
      }

      // ─── API: Get full project data ───
      if (url.pathname === "/api/project") {
        const projectName = url.searchParams.get("name");
        if (!projectName) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Missing ?name= parameter" }));
        }

        const s = await getStorageSafe();
        if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
        const context = await s.loadContext(projectName, "deep", PRISM_USER_ID);
        const ledger = await s.getLedgerEntries({
          project: `eq.${projectName}`,
          order: "created_at.desc",
          limit: "20",
        });
        let history: unknown[] = [];
        try {
          history = await s.getHistory(projectName, PRISM_USER_ID, 10);
        } catch {
          // History may not exist for all projects
        }

        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ context, ledger, history }));
      }

      // ─── API: Brain Health Check (v2.2.0) ───
      if (url.pathname === "/api/health" && req.method === "GET") {
        try {
          const { runHealthCheck } = await import("../utils/healthCheck.js");
          const s = await getStorageSafe();
          if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
          const stats = await s.getHealthStats(PRISM_USER_ID);
          const report = runHealthCheck(stats);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify(report));
        } catch (err) {
          console.error("[Dashboard] Health check error:", err);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({
            status: "unknown",
            summary: "Health check unavailable",
            issues: [],
            counts: { errors: 0, warnings: 0, infos: 0 },
            totals: { activeEntries: 0, handoffs: 0, rollups: 0 },
            timestamp: new Date().toISOString(),
          }));
        }
      }

      // ─── API: Brain Health Cleanup (v3.1) ───
      // Deletes orphaned handoffs and backfills missing embeddings.
      if (url.pathname === "/api/health/cleanup" && req.method === "POST") {
        try {
          const { runHealthCheck } = await import("../utils/healthCheck.js");
          const s = await getStorageSafe();
          if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
          const stats = await s.getHealthStats(PRISM_USER_ID);
          const report = runHealthCheck(stats);

          let repairedCount = 0;
          let failedCount = 0;
          let cleanupMessages: string[] = [];

          // 1. Backfill embeddings if missing
          const embeddingIssue = report.issues.find(i => i.check === "missing_embeddings");
          if (embeddingIssue && embeddingIssue.count > 0) {
            try {
              const { backfillEmbeddingsHandler } = await import("../tools/hygieneHandlers.js");
              let hasMore = true;
              let cursorId: string | undefined = undefined;
              let iterations = 0;
              const MAX_ITERATIONS = 100; // safety cap: 100 × 50 = 5000 entries max

              while (hasMore && iterations < MAX_ITERATIONS) {
                iterations++;
                const result: any = await backfillEmbeddingsHandler({ dry_run: false, limit: 50, _cursor_id: cursorId });
                const bStats = result._stats;
                if (bStats) {
                  repairedCount += bStats.repaired;
                  failedCount += bStats.failed;
                  if (bStats.last_id) cursorId = bStats.last_id;
                  else hasMore = false;
                  if ((bStats.repaired + bStats.failed) < 50) hasMore = false;
                } else {
                  hasMore = false;
                }
              }
              cleanupMessages.push(`Repaired ${repairedCount} embeddings`);
              if (failedCount > 0) cleanupMessages.push(`Failed to repair ${failedCount} embeddings`);
            } catch (err) {
              console.error("[Dashboard] Failed to backfill embeddings:", err);
              cleanupMessages.push("Embedding backfill failed");
            }
          }

          // 2. Collect orphaned handoff projects from the health issues
          const orphaned = stats.orphanedHandoffs || [];
          const cleaned: string[] = [];

          for (const { project } of orphaned) {
            try {
              await s.deleteHandoff(project, PRISM_USER_ID);
              cleaned.push(project);
              console.error(`[Dashboard] Cleaned up orphaned handoff: ${project}`);
            } catch (delErr) {
              console.error(`[Dashboard] Failed to delete handoff for ${project}:`, delErr);
            }
          }
          if (cleaned.length > 0) cleanupMessages.push(`Cleaned ${cleaned.length} orphaned handoffs`);

          const message = cleanupMessages.length > 0
            ? cleanupMessages.join(", ")
            : "No issues to clean up.";

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({
            ok: true,
            cleaned,
            repairedCount,
            count: cleaned.length + repairedCount,
            message,
          }));
        } catch (err) {
          const errMsg = err instanceof Error ? err.message : "Cleanup failed";
          console.error("[Dashboard] Health cleanup error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: false, error: errMsg, message: errMsg }));
        }
      }

      // ─── API: Role-Scoped Skills (v3.1) ───

      // GET /api/skills → { skills: { dev: "...", qa: "..." } }
      if (url.pathname === "/api/skills" && req.method === "GET") {
        const all = await getAllSettings();
        const skills: Record<string, string> = {};
        for (const [k, v] of Object.entries(all)) {
          if (k.startsWith("skill:") && v) {
            skills[k.replace("skill:", "")] = v;
          }
        }
        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ skills }));
      }

      // POST /api/skills → { role, content } saves user_skill:<role> (user-local namespace).
      // Platform skills (skill:*) are read-only — populated only by sync-skills.sh
      // or fetched from Synalux. Users cannot overwrite platform skills from the dashboard.
      if (url.pathname === "/api/skills" && req.method === "POST") {
        const body = await readBody(req);
        const { role, content } = JSON.parse(body || "{}");
        if (!role) { res.writeHead(400); return res.end(JSON.stringify({ error: "role required" })); }
        await setSetting(`user_skill:${role}`, content || "");
        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ ok: true, role, namespace: "user_skill" }));
      }

      // DELETE /api/skills/:role → clears user_skill:<role> (user-local only).
      if (url.pathname.startsWith("/api/skills/") && req.method === "DELETE") {
        const role = url.pathname.replace("/api/skills/", "");
        await setSetting(`user_skill:${role}`, "");
        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ ok: true, role, namespace: "user_skill" }));
      }

      // ─── API: Knowledge Graph (v6.2 — extracted to graphRouter.ts) ───
      if (url.pathname.startsWith("/api/graph")) {
        if (await handleGraphRoutes(url, req, res, getStorageSafe)) return;
      }

      // ─── API: Hivemind Team Roster (v3.0) ───
      if (url.pathname === "/api/team") {
        const projectName = url.searchParams.get("project");
        if (!projectName) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
        }
        try {
          const s = await getStorageSafe();
          if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
          const team = await s.listTeam(projectName, PRISM_USER_ID);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ team }));
        } catch {
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ team: [] }));
        }
      }

      // ─── API: Settings — GET (v3.0 Dashboard Settings) ───
      if (url.pathname === "/api/settings" && req.method === "GET") {
        try {
          const { getAllSettings } = await import("../storage/configStorage.js");
          const settings = await getAllSettings();
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ settings }));
        } catch {
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ settings: {} }));
        }
      }

      // ─── API: Settings — POST (v3.0 Dashboard Settings) ───
      if (url.pathname === "/api/settings" && req.method === "POST") {
        try {
          const body = await readBody(req);
          const parsed = JSON.parse(body);
          if (parsed.key && parsed.value !== undefined) {
            // SECURITY: Allowlist of dashboard-settable keys to prevent
            // credential overwrite (SUPABASE_KEY, STRIPE_SECRET_KEY, etc.)
            const SETTABLE_KEYS = new Set([
              "PRISM_STORAGE",
              "embedding_provider", "embedding_model",
              "PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
              "PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
              "PRISM_HDC_ENABLED", "PRISM_ACTR_ENABLED",
              "PRISM_GRAPH_PRUNING_ENABLED",
            ]);
            const isSkillKey = parsed.key.startsWith("skill:");
            const isTTLKey = parsed.key.startsWith("ttl:");
            const isAutoloadKey = parsed.key.startsWith("autoload:");
            if (!SETTABLE_KEYS.has(parsed.key) && !isSkillKey && !isTTLKey && !isAutoloadKey) {
              res.writeHead(403, { "Content-Type": "application/json" });
              return res.end(JSON.stringify({ error: `Setting key "${parsed.key}" is not allowed via the dashboard.` }));
            }
            const { setSetting } = await import("../storage/configStorage.js");
            await setSetting(parsed.key, String(parsed.value));
            res.writeHead(200, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ ok: true, key: parsed.key, value: parsed.value }));
          }
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Missing key or value" }));
        } catch (err) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Invalid JSON body" }));
        }

      }

      // ─── API: Memory Analytics (v3.1) ────────────────────
      if (url.pathname === "/api/analytics" && req.method === "GET") {
        const projectName = url.searchParams.get("project");
        if (!projectName) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
        }
        try {
          const s = await getStorageSafe();
          if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
          const analytics = await s.getAnalytics(projectName, PRISM_USER_ID);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify(analytics));
        } catch (err) {
          console.error("[Dashboard] Analytics error:", err);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({
            totalEntries: 0, totalRollups: 0, rollupSavings: 0,
            avgSummaryLength: 0, sessionsByDay: [],
          }));
        }
      }

      // ─── API: Retention (TTL) Settings (v3.1) ──────────────
      // GET /api/retention?project= → current TTL setting
      // POST /api/retention → { project, ttl_days } → saves + runs sweep
      if (url.pathname === "/api/retention") {
        if (req.method === "GET") {
          const projectName = url.searchParams.get("project");
          if (!projectName) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
          }
          const ttlRaw = await getSetting(`ttl:${projectName}`, "0");
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ project: projectName, ttl_days: parseInt(ttlRaw, 10) || 0 }));
        }

        if (req.method === "POST") {
          const body = await readBody(req);
          const { project, ttl_days } = JSON.parse(body || "{}");
          if (!project || ttl_days === undefined) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "project and ttl_days required" }));
          }
          if (ttl_days > 0 && ttl_days < 7) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Minimum TTL is 7 days" }));
          }
          await setSetting(`ttl:${project}`, String(ttl_days));
          let expired = 0;
          if (ttl_days > 0) {
            const s = await getStorageSafe();
            if (!s) { res.writeHead(503, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Storage initializing..." })); }
            const result = await s.expireByTTL(project, ttl_days, PRISM_USER_ID);
            expired = result.expired;
          }
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: true, project, ttl_days, expired }));
        }
      }

      // ─── API: Compact Now (v3.1 — Dashboard button) ──────────
      if (url.pathname === "/api/compact" && req.method === "POST") {
        const body = await readBody(req);
        const { project } = JSON.parse(body || "{}");
        if (!project) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "project required" }));
        }
        try {
          const result = await compactLedgerHandler({ project });
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: true, result }));
        } catch (err) {
          console.error("[Dashboard] Compact error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: false, error: "Compaction failed" }));
        }
      }

      // ─── API: PKM Export — Obsidian/Logseq ZIP (v3.1) ──────
      // ─── API: PKM Export (v6.1 — Prism-Port Vault) ──────────────
      // /api/export?project=<name>         — legacy URL (keeps old links working)
      // /api/export/vault?project=<name>  — canonical vault URL
      //
      // Both routes produce the same Prism-Port vault ZIP:
      //   - YAML frontmatter + Wikilinks (Obsidian/Logseq compatible)
      //   - Keyword backlink index (Keywords/<slug>.md)
      //   - Visual memory index (Visual_Memory/Index.md)
      //   - Settings/handoff metadata (Settings.md)
      //   - One file per ledger entry (Ledger/<date>_<slug>.md)
      if (
        (url.pathname === "/api/export" || url.pathname === "/api/export/vault") &&
        req.method === "GET"
      ) {
        const projectName = url.searchParams.get("project");
        if (!projectName) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
        }
        try {
          const s = await getStorageSafe();
          if (!s) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }

          // Gather data (mirrors sessionExportMemoryHandler)
          const ctx = await s.loadContext(projectName, "deep", PRISM_USER_ID) as Record<string, unknown> | null;
          const rawLedger = await s.getLedgerEntries({
            project: `eq.${projectName}`,
            order: "created_at.asc",
            limit: "1000",
          }) as Array<Record<string, unknown>>;

          // Strip binary embedding fields
          const cleanLedger = rawLedger.map(({ embedding: _e, embedding_compressed: _ec, ...rest }) => rest);

          const rawSettings = await getAllSettings();
          const safeSettings = redactSettings(rawSettings);
          const visualMemory = (ctx?.metadata as Record<string, unknown> | undefined)?.visual_memory as unknown[] ?? [];

          const exportPayload = {
            prism_export: {
              version: "6.1",
              exported_at: new Date().toISOString(),
              project: projectName,
              settings: safeSettings,
              handoff: ctx ?? null,
              visual_memory: visualMemory,
              ledger: cleanLedger,
            },
          };

          // Build vault directory and ZIP
          const { zipSync } = await import("fflate");
          const vaultFiles = buildVaultDirectory(exportPayload);
          const zipped = zipSync(vaultFiles, { level: 6 });

          res.writeHead(200, {
            "Content-Type": "application/zip",
            "Content-Disposition": `attachment; filename="prism-vault-${projectName}-${new Date().toISOString().slice(0, 10)}.zip"`,
            "Content-Length": String(zipped.byteLength),
          });
          return res.end(Buffer.from(zipped));
        } catch (err) {
          console.error("[Dashboard] Vault export error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Export failed" }));
        }
      }


      // ─── API: Universal History Import (v5.2) ───
      // NOTE: Transactional safety (BEGIN/COMMIT/ROLLBACK) is handled inside
      // universalImporter — each conversation is inserted atomically.
      // The dashboard handler provides HTTP-level error handling only.
      if (url.pathname === "/api/import" && req.method === "POST") {
        try {
          const body = await readBody(req);
          const { path: filePath, format, project, dryRun } = JSON.parse(body || "{}");
          if (!filePath) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "path is required" }));
          }

          // SECURITY: Restrict import paths to prevent arbitrary file reads.
          // Only allow files from home directory, /tmp, and current working directory.
          const resolvedPath = path.resolve(filePath);
          const homeDir = os.homedir();
          const allowedPrefixes = [path.join(homeDir, ".prism-mcp", "imports"), os.tmpdir()];
          const isAllowed = allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix + path.sep) || resolvedPath === prefix);
          if (!isAllowed) {
            res.writeHead(403, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: `Import path must be under home directory or /tmp` }));
          }

          // Verify file exists before starting import
          if (!fs.existsSync(filePath)) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: `File not found: ${filePath}` }));
          }

          const { universalImporter } = await import("../utils/universalImporter.js");
          const result = await universalImporter({
            path: filePath,
            format: format || undefined,
            project: project || undefined,
            dryRun: !!dryRun,
            verbose: false,
          });

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({
            ok: true,
            ...result,
            message: `Imported ${result.conversationCount} conversations (${result.successCount} turns)${result.skipCount > 0 ? `, ${result.skipCount} skipped (dup)` : ""}${result.failCount > 0 ? `, ${result.failCount} failed` : ""}${dryRun ? " [DRY RUN]" : ""}`,
          }));
        } catch (err: any) {
          console.error("[Dashboard] Import error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: err.message || "Import failed" }));
        }
      }

      // ─── API: Universal History Import via File Upload (v5.2) ───
      if (url.pathname === "/api/import-upload" && req.method === "POST") {
        try {
          const body = await readBody(req);
          const { filename, content, format, project, dryRun } = JSON.parse(body || "{}");
          if (!content || !filename) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "filename and content are required" }));
          }

          // Write uploaded content to a temp file
          const tmpDir = path.join(os.tmpdir(), "prism-import");
          fs.mkdirSync(tmpDir, { recursive: true });
          const safeFilename = path.basename(filename); // prevent path traversal
          const tmpFile = path.join(tmpDir, `upload-${Date.now()}-${safeFilename}`);
          fs.writeFileSync(tmpFile, content, "utf-8");

          try {
            const { universalImporter } = await import("../utils/universalImporter.js");
            const result = await universalImporter({
              path: tmpFile,
              format: format || undefined,
              project: project || undefined,
              dryRun: !!dryRun,
              verbose: false,
            });

            res.writeHead(200, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({
              ok: true,
              ...result,
              message: `Imported ${result.conversationCount} conversations (${result.successCount} turns)${result.skipCount > 0 ? `, ${result.skipCount} skipped (dup)` : ""}${result.failCount > 0 ? `, ${result.failCount} failed` : ""}${dryRun ? " [DRY RUN]" : ""} from ${filename}`,
            }));
          } finally {
            // Clean up temp file
            try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
          }
        } catch (err: any) {
          console.error("[Dashboard] Import upload error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: err.message || "Import failed" }));
        }
      }

      // ─── API: Background Scheduler Status (v5.4) ────────────
      if (url.pathname === "/api/scheduler" && req.method === "GET") {
        try {
          const { getSchedulerStatus } = await import("../backgroundScheduler.js");
          const status = getSchedulerStatus();
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify(status));
        } catch (err) {
          console.error("[Dashboard] Scheduler status error:", err);
          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({
            running: false, startedAt: null, intervalMs: 0, lastSweep: null,
          }));
        }
      }

      // ─── API: Autonomous Web Scholar Trigger (v5.4) ─────────
      if (url.pathname === "/api/scholar/trigger" && req.method === "POST") {
        try {
          const { runWebScholar } = await import("../scholar/webScholar.js");

          // Fire and forget, don't block the request
          runWebScholar().catch(err => {
            console.error("[Dashboard] Web Scholar async trigger failed:", err);
          });

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: true, message: "Autonomous research started in background" }));
        } catch (err: any) {
          console.error("[Dashboard] Web Scholar trigger error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: err.message || "Failed to trigger Web Scholar" }));
        }
      }

      // ─── API: Semantic Vector Search (v6.0) ───
      if (url.pathname === "/api/search" && req.method === "GET") {
        try {
          const q = url.searchParams.get("q");
          if (!q) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Missing ?q= parameter" }));
          }

          const project = url.searchParams.get("project") || null;
          const limit = Number(url.searchParams.get("limit")) || 10;
          const offset = Number(url.searchParams.get("offset")) || 0;
          const similarityThreshold = parseFloat(url.searchParams.get("threshold") || "0.6");
          const contextBoost = url.searchParams.get("boost") === "true";

          const s = await getStorageSafe();
          if (!s) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }

          let queryText: string = q as string;
          if (contextBoost && project) {
            const context = await s!.loadContext(project as string, "quick", PRISM_USER_ID);
            if (context) queryText = `${project} scope: ${JSON.stringify(context)}\nQuery: ${q}`;
          }

          // Check LLM provider availability before attempting embedding
          let llm;
          try {
            llm = getLLMProvider();
          } catch {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "LLM Provider not configured for semantic search. Configure an embedding provider in the Mind Palace dashboard." }));
          }

          const queryEmbedding = await llm.generateEmbedding(queryText);

          // We query limit + offset, then slice manually since the storage
          // layer interface limit parameter doesn't natively expose offset.
          const results = await s!.searchMemory({
            queryEmbedding: JSON.stringify(queryEmbedding),
            project: project || undefined,
            limit: limit + offset,
            similarityThreshold,
            userId: PRISM_USER_ID,
          });

          // Slice to emulate offset for pagination
          const paginated = results.slice(offset, offset + limit);

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ query: q, project, results: paginated }));
        } catch (err: any) {
          console.error("[Dashboard] Search error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: err.message || "Failed to search memory" }));
        }
      }


      // ─── API: Dark Factory Pipelines (v7.3) ───────────────────

      // GET /api/pipelines — List pipelines (optional ?status=RUNNING&project=myproj)
      if (url.pathname === "/api/pipelines" && req.method === "GET") {
        try {
          const s = await getStorageSafe();
          if (!s) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }

          // Validate status filter against canonical set
          var rawStatus = url.searchParams.get("status") || undefined;
          var VALID_STATUSES = ['PENDING', 'RUNNING', 'PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'];
          if (rawStatus && VALID_STATUSES.indexOf(rawStatus) === -1) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: 'Invalid status filter: "' + rawStatus + '". Valid: ' + VALID_STATUSES.join(', ') }));
          }
          var statusFilter = rawStatus as import("../storage/interface.js").PipelineState["status"] | undefined;
          const projectFilter = url.searchParams.get("project") || undefined;

          const pipelines = await s.listPipelines(projectFilter, statusFilter, PRISM_USER_ID);

          // Parse spec JSON for frontend consumption
          const enriched = pipelines.map((p: any) => {
            let parsedSpec = null;
            try { parsedSpec = JSON.parse(p.spec); } catch { /* corrupt spec */ }
            return { ...p, parsedSpec };
          });

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ pipelines: enriched }));
        } catch (err) {
          console.error("[Dashboard] Pipeline list error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Failed to list pipelines" }));
        }
      }

      // GET /api/pipelines/:id — Single pipeline detail
      if (url.pathname.startsWith("/api/pipelines/") && !url.pathname.includes("/abort") && req.method === "GET") {
        try {
          const pipelineId = url.pathname.replace("/api/pipelines/", "");
          if (!pipelineId) {
            res.writeHead(400, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Missing pipeline ID" }));
          }

          const s = await getStorageSafe();
          if (!s) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }

          const pipeline = await s.getPipeline(pipelineId, PRISM_USER_ID);
          if (!pipeline) {
            res.writeHead(404, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Pipeline not found" }));
          }

          let parsedSpec = null;
          try { parsedSpec = JSON.parse(pipeline.spec); } catch { /* corrupt spec */ }

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ...pipeline, parsedSpec }));
        } catch (err) {
          console.error("[Dashboard] Pipeline detail error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Failed to get pipeline" }));
        }
      }

      // POST /api/pipelines/:id/abort — Dashboard kill switch
      if (url.pathname.match(/^\/api\/pipelines\/[^/]+\/abort$/) && req.method === "POST") {
        try {
          const pipelineId = url.pathname.replace("/api/pipelines/", "").replace("/abort", "");

          const s = await getStorageSafe();
          if (!s) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }

          const pipeline = await s.getPipeline(pipelineId, PRISM_USER_ID);
          if (!pipeline) {
            res.writeHead(404, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Pipeline not found" }));
          }

          // Already terminated?
          if (["COMPLETED", "FAILED", "ABORTED"].includes(pipeline.status)) {
            res.writeHead(200, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ ok: true, status: pipeline.status, message: `Pipeline already in terminal state: ${pipeline.status}` }));
          }

          await s.savePipeline({
            ...pipeline,
            status: "ABORTED",
            error: "Manually aborted via dashboard kill switch.",
          });

          console.error(`[Dashboard] Pipeline ${pipelineId} aborted via dashboard.`);

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ ok: true, status: "ABORTED", message: "Pipeline aborted. Runner will stop on next tick." }));
        } catch (err) {
          console.error("[Dashboard] Pipeline abort error:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Failed to abort pipeline" }));
        }
      }


      if (url.pathname === "/manifest.json" && req.method === "GET") {
        const manifest = {
          name: "Prism Mind Palace",
          short_name: "Prism",
          description: "Prism MCP Mobile Dashboard",
          start_url: "/",
          display: "standalone",
          background_color: "#0a0e1a",
          theme_color: "#0a0e1a",
          icons: [
            { src: "/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any" },
            { src: "/icon-192-maskable.svg", sizes: "192x192", type: "image/svg+xml", purpose: "maskable" },
            { src: "/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any" },
            { src: "/icon-512-maskable.svg", sizes: "512x512", type: "image/svg+xml", purpose: "maskable" }
          ]
        };
        res.writeHead(200, {
          "Content-Type": "application/json",
          "Cache-Control": "public, max-age=86400"
        });
        return res.end(JSON.stringify(manifest));
      }

      // ─── PWA: Service Worker (v5.4) ───
      if (url.pathname === "/sw.js" && req.method === "GET") {
        const swContent = `
const CACHE_NAME = 'prism-pwa-v2.1';
const ASSETS = [
  '/',
  '/manifest.json',
  '/icon-192.svg',
  '/icon-192-maskable.svg',
  '/icon-512.svg',
  '/icon-512-maskable.svg',
  '/apple-touch-icon.png',
  '/offline.html'
];

self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(CACHE_NAME).then((c) => c.addAll(ASSETS)));
  self.skipWaiting();
});

self.addEventListener('activate', (e) => {
  e.waitUntil(caches.keys().then((keys) => {
    return Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)));
  }));
  self.clients.claim();
});

self.addEventListener('fetch', (e) => {
  if (e.request.method !== 'GET') return;
  // Serve offline page for HTML navigation
  if (e.request.mode === 'navigate') {
    e.respondWith(fetch(e.request).catch(() => caches.match('/offline.html')));
    return;
  }
  // Network-first for API requests, Cache-first for Assets
  if (e.request.url.includes('/api/')) {
    e.respondWith(fetch(e.request).catch(() => new Response(JSON.stringify({ error: "Offline" }), { headers: { "Content-Type": "application/json" }, status: 503 })));
  } else {
    e.respondWith(caches.match(e.request).then((res) => res || fetch(e.request).then((fres) => {
      // Cache dynamically fetched non-API assets
      return caches.open(CACHE_NAME).then(c => { c.put(e.request, fres.clone()); return fres; });
    }).catch(() => null)));
  }
});

self.addEventListener('message', (e) => {
  if (e.data && e.data.action === 'skipWaiting') {
    self.skipWaiting();
  }
});
        `.trim();
        res.writeHead(200, {
          "Content-Type": "application/javascript",
          "Cache-Control": "no-cache"
        });
        return res.end(swContent);
      }

      // ─── PWA: Offline Fallback HTML (v5.4) ───
      if (url.pathname === "/offline.html" && req.method === "GET") {
        const offlineHtml = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Prism MCP — Offline</title>
  <link rel="manifest" href="/manifest.json">
  <meta name="theme-color" content="#0a0e1a">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      background: #0a0e1a;
      color: #f1f5f9;
      font-family: 'Inter', sans-serif;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      padding: 2rem;
    }
    .bg {
      position: fixed;
      inset: 0;
      background-image:
        radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.08) 0%, transparent 50%),
        radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.06) 0%, transparent 50%);
      z-index: 0;
    }
    .card {
      position: relative;
      z-index: 1;
      background: rgba(17, 24, 39, 0.6);
      backdrop-filter: blur(16px);
      border: 1px solid rgba(139, 92, 246, 0.15);
      border-radius: 16px;
      padding: 3rem 2rem;
      max-width: 400px;
      width: 100%;
    }
    .icon {
      font-size: 3rem;
      margin-bottom: 1.5rem;
      opacity: 0.8;
      filter: drop-shadow(0 0 10px rgba(139, 92, 246, 0.5));
    }
    h1 {
      font-size: 1.5rem;
      font-weight: 600;
      margin-bottom: 1rem;
      background: linear-gradient(135deg, #8b5cf6, #3b82f6);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    p {
      color: #94a3b8;
      font-size: 0.95rem;
      line-height: 1.5;
      margin-bottom: 2rem;
    }
    button {
      background: linear-gradient(135deg, #8b5cf6, #3b82f6);
      color: white;
      border: none;
      padding: 0.75rem 1.5rem;
      border-radius: 8px;
      font-size: 0.95rem;
      font-weight: 600;
      cursor: pointer;
      font-family: 'Inter', sans-serif;
      transition: opacity 0.2s;
    }
    button:hover { opacity: 0.9; }
  </style>
</head>
<body>
  <div class="bg"></div>
  <div class="card">
    <div class="icon">🔌</div>
    <h1>You are currently offline.</h1>
    <p>The Prism Dashboard cannot reach the MCP server. Please check your internet connection to resume.</p>
    <button onclick="window.location.reload()">Try Again</button>
  </div>
</body>
</html>`;
        res.writeHead(200, {
          "Content-Type": "text/html",
          "Cache-Control": "public, max-age=86400"
        });
        return res.end(offlineHtml);
      }

      // ─── PWA: Dynamic SVG Icons (v5.4) ───
      if ((url.pathname === "/icon-192.svg" || url.pathname === "/icon-512.svg" || url.pathname === "/icon-192-maskable.svg" || url.pathname === "/icon-512-maskable.svg") && req.method === "GET") {
        const size = url.pathname.includes("192") ? 192 : 512;
        const isMaskable = url.pathname.includes("maskable");
        // For standard "any" icons, we might want rounded corners or a specific size ratio if needed,
        // but since we separate purposes, we can keep the SVG identical as adaptive scaling is handled by OS
        const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}">
  <defs>
    <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#8b5cf6" />
      <stop offset="50%" stop-color="#3b82f6" />
      <stop offset="100%" stop-color="#06b6d4" />
    </linearGradient>
  </defs>
  <rect width="${size}" height="${size}" rx="${isMaskable ? 0 : Math.floor(size * 0.2)}" fill="#0a0e1a"/>
  <path d="M${size * 0.5} ${size * 0.25} L${size * 0.75} ${size * 0.75} L${size * 0.25} ${size * 0.75} Z" fill="url(#grad)" opacity="0.9"/>
  <circle cx="${size * 0.5}" cy="${size * 0.55}" r="${size * 0.15}" fill="#ffffff" opacity="0.1" />
</svg>`;
        res.writeHead(200, {
          "Content-Type": "image/svg+xml",
          "Cache-Control": "public, max-age=86400"
        });
        return res.end(svg);
      }

      // ─── PWA: iOS Apple Touch Icon (v5.5) ───
      if (url.pathname === "/apple-touch-icon.png" && req.method === "GET") {
        // iOS Safari does not support SVG for apple-touch-icon; requires PNG.
        // We serve a robust Base64-encoded 180x180 opaque PNG to prevent the "black box" issue.
        const pngBase64 = " ";
        res.writeHead(200, {
          "Content-Type": "image/png",
          "Cache-Control": "public, max-age=86400"
        });
        return res.end(Buffer.from(pngBase64, "base64"));
      }

      // ─── API: Intent Health (Feature A) ───
      // GET /api/intent-health?project=xxx
      if (url.pathname === "/api/intent-health" && req.method === "GET") {
        const project = url.searchParams.get("project");
        if (!project) {
          res.writeHead(400, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "project is required" }));
        }

        try {
          const storage = await getStorageSafe();
          if (!storage) {
            res.writeHead(503, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Storage initializing..." }));
          }
          // Load 'standard' context to get active_decisions and recent_sessions
          // Note: API MUST use "standard" level (deep skips recent_sessions, which breaks staleness).
          const ctx = await storage.loadContext(project, "standard", PRISM_USER_ID) as any;

          if (!ctx) {
            res.writeHead(404, { "Content-Type": "application/json" });
            return res.end(JSON.stringify({ error: "Project not found" }));
          }

          // 1. Fetch configurable threshold (default 30 days, avoiding 7-day panic)
          const staleThresholdDays = parseInt(getSettingSync("intent_health_stale_threshold_days", "30"), 10) || 30;

          const healthResult = computeIntentHealth(ctx, staleThresholdDays);

          res.writeHead(200, { "Content-Type": "application/json" });
          return res.end(JSON.stringify(healthResult));
        } catch (err: any) {
          console.error("[intent-health] Error computing intent health:", err);
          res.writeHead(500, { "Content-Type": "application/json" });
          return res.end(JSON.stringify({ error: "Failed to compute intent health" }));
        }
      }

      // ─── 404 ───
      res.writeHead(404, { "Content-Type": "text/plain" });
      res.end("Not found");


    } catch (error) {
      console.error("[Dashboard] Error handling request:", error);
      res.writeHead(500, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Internal Server Error" }));
    }
  });

  // ─── Resilient port binding with retry ───
  // Wraps listen() in a Promise to detect EADDRINUSE failures and retry
  // with a delay (gives OS time to release the port).
  // Falls back to PORT+1, PORT+2 if the preferred port is permanently taken.
  const MAX_RETRIES = 3;
  const RETRY_DELAY_MS = 500;

  const tryListen = (port: number): Promise<number> =>
    new Promise((resolve, reject) => {
      const onError = (err: NodeJS.ErrnoException) => {
        httpServer.removeListener("error", onError);
        reject(err);
      };
      httpServer.on("error", onError);
      const bindHost = AUTH_ENABLED ? "0.0.0.0" : "127.0.0.1";
      httpServer.listen(port, bindHost, () => {
        httpServer.removeListener("error", onError);
        // Re-register a permanent error handler for runtime errors
        httpServer.on("error", (err: NodeJS.ErrnoException) => {
          console.error(`[Dashboard] HTTP server error: ${err.message}`);
        });
        resolve(port);
      });
    });

  let boundPort = PORT;
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
    try {
      boundPort = await tryListen(PORT + attempt);
      break; // Success
    } catch (err: any) {
      if (err.code === "EADDRINUSE") {
        console.error(
          `[Dashboard] Port ${PORT + attempt} is in use (attempt ${attempt + 1}/${MAX_RETRIES}).`
        );
        if (attempt < MAX_RETRIES - 1) {
          // Wait for OS to release the port, then try next port
          await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
        } else {
          console.error(
            `[Dashboard] All ports ${PORT}–${PORT + MAX_RETRIES - 1} in use — Mind Palace disabled. ` +
            `Set PRISM_DASHBOARD_PORT to use a different port.`
          );
          return; // Give up — MCP server keeps running
        }
      } else {
        console.error(`[Dashboard] HTTP server error: ${err.message}`);
        return; // Non-retryable error
      }
    }
  }

  // Write the active port to a file for discoverability
  try {
    const portFile = path.join(os.homedir(), ".prism-mcp", "dashboard.port");
    fs.writeFileSync(portFile, String(boundPort), "utf8");
  } catch {
    // Non-fatal — just means the user has to know the port
  }

  console.error(`[Prism] 🧠 Mind Palace Dashboard → http://localhost:${boundPort}`);

  // ─── v3.1: TTL Sweep — runs at startup + every 12 hours ───────────
  // NOTE (v5.4): The Background Scheduler in server.ts now also handles
  // TTL sweeps. This dashboard sweep is kept as a legacy fallback for
  // deployments where the scheduler is disabled. Both are idempotent.
  async function runTtlSweep() {
    try {
      const allSettings = await getAllSettings();
      for (const [key, val] of Object.entries(allSettings)) {
        if (!key.startsWith("ttl:")) continue;
        const project = key.replace("ttl:", "");
        const ttlDays = parseInt(val, 10);
        if (!ttlDays || ttlDays <= 0) continue;
        const s = await getStorageSafe();
        if (!s) continue;
        const result = await s.expireByTTL(project, ttlDays, PRISM_USER_ID);
        if (result.expired > 0) {
          console.error(`[Dashboard] TTL sweep: expired ${result.expired} entries for "${project}" (ttl=${ttlDays}d)`);
        }
      }
    } catch (err) {
      console.error("[Dashboard] TTL sweep error (non-fatal):", err);
    }
  }

  // Run immediately on startup, then every 12 hours
  runTtlSweep().catch(() => {});
  setInterval(() => { runTtlSweep().catch(() => {}); }, 12 * 60 * 60 * 1000);

  // Prune expired dashboard sessions every hour
  setInterval(() => {
    const now = Date.now();
    for (const [token, expiry] of activeSessions) {
      if (now > expiry) activeSessions.delete(token);
    }
  }, 60 * 60 * 1000);
}

