"""HTTP notification feed + Kanban board — browser-based UI for the Vastian Network.

Serves two tabs:
  1. Activity Feed — live tasks, notifications, expandable history
  2. Kanban Board — agent-suggested tasks with approve/reject checkboxes

Designed to persist beyond CLI exit (daemon thread, JS polling, no meta-refresh).
"""

from __future__ import annotations

import json
import logging
import threading
from datetime import UTC
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.parse import parse_qs

logger = logging.getLogger(__name__)

_PROJECT_ROOT = Path(__file__).resolve().parents[3]
_DOCS_ROOT = _PROJECT_ROOT / "vastian-docs"


def _boards_path() -> Path:
    from vastian.storage.data_root import get_config_dir

    return get_config_dir(_PROJECT_ROOT) / "boards" / "boards.yaml"


def _load_boards_config() -> tuple[dict[str, str], dict[str, str]]:
    """Load board colors and labels from config/boards/boards.yaml. Returns (colors, labels)."""
    fallback_colors = {
        "user": "#58a6ff",
        "comms": "#d29922",
        "company": "#a371f7",
        "agent": "#3fb950",
        "ops": "#f85149",
    }
    fallback_labels = {
        "user": "User (USR-)",
        "comms": "Comms (MSG-)",
        "company": "Company (REL-)",
        "agent": "Agent (CYC-)",
        "ops": "Ops (OPS-)",
    }
    if not _boards_path().exists():
        return fallback_colors, fallback_labels
    try:
        import yaml

        cfg = yaml.safe_load(_boards_path().read_text(encoding="utf-8")) or {}
        colors: dict[str, str] = {}
        labels: dict[str, str] = {}
        for bid, b in (cfg.get("boards") or {}).items():
            colors[bid] = b.get("border_color") or b.get(
                "color", fallback_colors.get(bid, "#8b949e")
            )
            labels[bid] = b.get("label", bid)
        for sb in cfg.get("scenario_boards") or []:
            bid = sb.get("id", "")
            if bid:
                colors[bid] = sb.get("border_color") or sb.get("color", "#8b949e")
                labels[bid] = sb.get(
                    "label", bid.replace("scenario-", "").replace("_", " ").title()
                )
        if not colors:
            return fallback_colors, fallback_labels
        for k, v in fallback_colors.items():
            if k not in colors:
                colors[k] = v
        for k, v in fallback_labels.items():
            if k not in labels:
                labels[k] = v
        return colors, labels
    except Exception:
        return fallback_colors, fallback_labels


def _get_theme_css(project_root=None) -> str:
    """Load ui-preferences from config (vastian data path) and return :root CSS vars."""
    from vastian.ui_theme import get_theme_css

    base = get_theme_css(project_root)
    return base + (
        ".k-card:hover { border-color: var(--accent); }\n"
        ".card .agent, .history-row .h-agent { color: var(--accent); }\n"
        ".section-title { color: var(--accent); }\n"
    )


def _docs_wrapper(slug: str, body: str) -> str:
    """Wrap doc body in a minimal HTML shell."""
    title = slug.replace("-", " ").replace("_", " ").title()
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} — Vastian Docs</title>
<style>
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
  body {{ font-family: 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.6; }}
  .top {{ background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px; }}
  .top a {{ color: #58a6ff; text-decoration: none; }}
  .top a:hover {{ text-decoration: underline; }}
  .main {{ max-width: 800px; margin: 0 auto; padding: 24px; }}
  h1 {{ color: #58a6ff; margin-bottom: 16px; font-size: 1.5em; }}
  h2 {{ color: #c9d1d9; margin: 24px 0 12px; font-size: 1.2em; border-bottom: 1px solid #21262d; padding-bottom: 6px; }}
  h3 {{ color: #c9d1d9; margin: 16px 0 8px; font-size: 1.05em; }}
  p {{ margin-bottom: 12px; }}
  code {{ background: #161b22; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }}
  pre {{ background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; font-size: 0.88em; }}
  pre code {{ background: none; padding: 0; }}
  ul, ol {{ margin: 12px 0 12px 24px; }}
  a {{ color: #58a6ff; }}
</style>
</head>
<body>
<div class="top"><a href="/docs">← Docs</a></div>
<div class="main">{body}</div>
</body>
</html>"""


def _docs_404_html() -> str:
    return _docs_wrapper(
        "Not Found", "<h1>Not Found</h1><p>The requested documentation page does not exist.</p>"
    )


# ── Agent metadata for the command-help drawer ─────────────────────
AGENT_META: list[dict] = [
    # ── Level 0: Chief of Staff (always visible, top-level coordinator) ──
    {
        "id": "charles",
        "name": "Charles",
        "title": "Chief of Staff",
        "level": 0,
        "tier": "chief",
        "topics": ["scheduling", "briefing", "triage", "dashboard"],
    },
    # ── Level 1: Strategic / C-Suite (high prominence, user-facing leadership) ──
    {
        "id": "victor",
        "name": "Victor",
        "title": "Vision & Life Strategy",
        "level": 1,
        "tier": "strategic",
        "topics": ["vision", "values", "life-strategy", "health"],
    },
    {
        "id": "dominic",
        "name": "Dominic",
        "title": "Strategy Advisor",
        "level": 1,
        "tier": "strategic",
        "topics": ["strategy", "roadmap", "spikes", "planning"],
    },
    {
        "id": "finley",
        "name": "Finley",
        "title": "Finance Advisor",
        "level": 1,
        "tier": "strategic",
        "topics": ["finance", "budget", "savings", "salary"],
    },
    # ── Level 2: Specialists (accessible, user can engage directly) ──
    {
        "id": "maya",
        "name": "Maya",
        "title": "Meeting Facilitator",
        "level": 2,
        "tier": "specialist",
        "topics": ["meetings", "signals", "transcripts", "action-items"],
    },
    {
        "id": "sage",
        "name": "Sage",
        "title": "Mentor & Wellness Guide",
        "level": 2,
        "tier": "specialist",
        "topics": ["career", "mentors", "wellbeing", "growth-plans"],
    },
    {
        "id": "noah",
        "name": "Noah",
        "title": "Creative Director",
        "level": 2,
        "tier": "specialist",
        "topics": ["sessions", "creative", "naming", "curation"],
    },
    {
        "id": "concord",
        "name": "Concord",
        "title": "Simulation Analyst",
        "level": 2,
        "tier": "specialist",
        "topics": ["simulations", "what-if", "scenarios", "forecasting"],
    },
    {
        "id": "adrian",
        "name": "Adrian",
        "title": "Personal Growth Guide",
        "level": 2,
        "tier": "specialist",
        "topics": ["personas", "self-knowledge", "exercises", "growth"],
    },
    {
        "id": "jake",
        "name": "Jake",
        "title": "Metrics Analyst",
        "level": 2,
        "tier": "specialist",
        "topics": ["metrics", "assessment", "scoring", "validation"],
    },
    {
        "id": "kiran",
        "name": "Kiran",
        "title": "Scenario Lab Lead",
        "level": 2,
        "tier": "specialist",
        "topics": ["cross-domain", "testing", "metrics", "scenarios"],
    },
    {
        "id": "rowan",
        "name": "Rowan",
        "title": "Digital Twin",
        "level": 2,
        "tier": "specialist",
        "topics": ["values", "decisions", "council", "identity"],
    },
    # ── Level 3: Backend (work behind the scenes, not user-facing) ──
    {
        "id": "henry",
        "name": "Henry",
        "title": "Technical Lead",
        "level": 3,
        "tier": "backend",
        "topics": ["architecture", "code", "data-engineering", "technical"],
    },
    {
        "id": "liam",
        "name": "Liam",
        "title": "Project Coordinator",
        "level": 2,
        "tier": "specialist",
        "topics": ["cycles", "tasks", "execution", "delegation"],
    },
    {
        "id": "arjuna",
        "name": "Arjuna",
        "title": "Sprint Manager",
        "level": 3,
        "tier": "backend",
        "topics": ["sprints", "tickets", "decomposition", "kanban"],
    },
    {
        "id": "theo",
        "name": "Theo",
        "title": "Quality Lead",
        "level": 3,
        "tier": "backend",
        "topics": ["quality", "prompts", "audits", "issues"],
    },
    {
        "id": "elias",
        "name": "Elias",
        "title": "Knowledge Manager",
        "level": 3,
        "tier": "backend",
        "topics": ["knowledge", "research", "VKB", "memory"],
    },
    {
        "id": "orion",
        "name": "Orion",
        "title": "Operations Lead",
        "level": 3,
        "tier": "backend",
        "topics": ["operations", "processes", "coordination", "systems"],
    },
    {
        "id": "felix",
        "name": "Felix",
        "title": "System Monitor",
        "level": 3,
        "tier": "backend",
        "topics": ["reliability", "monitoring", "uptime", "health-checks"],
    },
    {
        "id": "maria",
        "name": "Maria",
        "title": "Integration Lead",
        "level": 3,
        "tier": "backend",
        "topics": ["integrations", "relay", "config-cards", "external"],
    },
]

CLI_COMMANDS: list[dict] = [
    {"cmd": "/agent <id>", "desc": "Switch to a specific agent", "topic": "navigation"},
    {"cmd": "/auto", "desc": "Re-enable auto-routing (Charles)", "topic": "navigation"},
    {"cmd": "/onboard", "desc": "Run onboarding from saved_prompts/intro.txt", "topic": "setup"},
    {
        "cmd": "/council <topic>",
        "desc": "Convene a Mentor Council meeting",
        "topic": "collaboration",
    },
    {"cmd": "/audit <id>", "desc": "Have Theo audit an agent's prompt quality", "topic": "quality"},
    {"cmd": "/tasks", "desc": "Show delegations and pending tasks", "topic": "tasks"},
    {"cmd": "/tools [id]", "desc": "Show agent's available tools", "topic": "tools"},
    {
        "cmd": "/simulate <desc>",
        "desc": "Run a what-if simulation via Concord",
        "topic": "simulations",
    },
    {"cmd": "/assess <vars>", "desc": "Run a metric assessment via Jake", "topic": "metrics"},
    {"cmd": "/metrics", "desc": "Show all simulation variables and metrics", "topic": "metrics"},
    {"cmd": "/models", "desc": "Show available LLM models and provider status", "topic": "system"},
    {"cmd": "/bg", "desc": "Show background task status", "topic": "tasks"},
    {
        "cmd": "/report <id>",
        "desc": "Detailed delegation report (task ID or agent ID)",
        "topic": "tasks",
    },
    {"cmd": "/inbox", "desc": "Show inbox watcher status and folder counts", "topic": "inbox"},
    {"cmd": "/feed", "desc": "Show notification feed URL", "topic": "system"},
    {"cmd": "/check-in", "desc": "See agent outreach and reminders", "topic": "collaboration"},
    {
        "cmd": "/check-in -l",
        "desc": "Interactive list — pick an item to address",
        "topic": "collaboration",
    },
    {
        "cmd": "/check-in -k",
        "desc": "Also show Kanban tasks needing attention",
        "topic": "collaboration",
    },
    {
        "cmd": "/skip",
        "desc": "Snooze (high priority) or dismiss (normal) outreach",
        "topic": "collaboration",
    },
    {
        "cmd": "/dismiss",
        "desc": "Permanently dismiss outreach (no snooze)",
        "topic": "collaboration",
    },
    {
        "cmd": "/clear-failed",
        "desc": "Dismiss all stale failed tasks from sync suggestions",
        "topic": "tasks",
    },
    {
        "cmd": "/meeting <team>",
        "desc": "Convene a team meeting (e.g., vv-dev)",
        "topic": "collaboration",
    },
    {
        "cmd": "/meeting <team> --twin",
        "desc": "Send your digital twin to a meeting",
        "topic": "collaboration",
    },
    {
        "cmd": "/assign-me <id>",
        "desc": "Claim a suggested Kanban task for yourself",
        "topic": "tasks",
    },
    {"cmd": "/done <id>", "desc": "Mark a Kanban task as done by its ID", "topic": "tasks"},
    {"cmd": "/pending", "desc": "List pending transaction approvals", "topic": "finance"},
    {"cmd": "/approve <id>", "desc": "Approve a pending transaction", "topic": "finance"},
    {"cmd": "/deny <id>", "desc": "Deny a pending transaction", "topic": "finance"},
    {"cmd": "/improve", "desc": "Run Orion's codebase improvement scan", "topic": "operations"},
    {"cmd": "/suggest [id]", "desc": "Ask agents to suggest Kanban tasks", "topic": "tasks"},
    {"cmd": "/escalate", "desc": "Show escalation pathways", "topic": "system"},
    {"cmd": "/agents", "desc": "List all agents", "topic": "navigation"},
    {"cmd": "/status", "desc": "Show network status", "topic": "system"},
    {
        "cmd": "/sessions",
        "desc": "Open sessions roster (session types + history)",
        "topic": "system",
    },
    {
        "cmd": "/personas",
        "desc": "Open persona radar chart (12 archetypes + traits)",
        "topic": "system",
    },
    {
        "cmd": "/settings",
        "desc": "Open global settings (automation frequency, tiers)",
        "topic": "system",
    },
    {"cmd": "/help", "desc": "Show command help", "topic": "navigation"},
]

FEED_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Vastian Network</title>
<style>
  __THEME_CSS__
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    background: #0d1117; color: #c9d1d9;
  }

  /* ── Tab navigation ────────────────────────────── */
  .tab-bar {
    display: flex; background: #161b22; border-bottom: 1px solid #30363d;
    padding: 0 16px; position: sticky; top: 0; z-index: 100;
  }
  .tab-btn {
    padding: 12px 20px; color: #8b949e; cursor: pointer;
    border: none; background: none; font-size: 0.9em; font-family: inherit;
    border-bottom: 2px solid transparent; transition: all 0.15s;
  }
  .tab-btn:hover { color: #c9d1d9; }
  .tab-btn.active { color: #58a6ff; border-bottom-color: #58a6ff; }
  .tab-content { display: none; padding: 20px; padding-right: 60px; }
  .tab-content.active { display: block; }

  /* ── Shared styles ─────────────────────────────── */
  h1 { color: #58a6ff; margin-bottom: 8px; font-size: 1.4em; }
  .subtitle { color: #8b949e; margin-bottom: 16px; font-size: 0.85em; }
  .section-title {
    color: #58a6ff; font-size: 1.05em; margin: 16px 0 8px;
    border-bottom: 1px solid #21262d; padding-bottom: 6px;
    display: flex; justify-content: space-between; align-items: center;
  }
  .empty { color: #484f58; padding: 16px; text-align: center; }
  .badge {
    font-size: 0.7em; padding: 2px 8px; border-radius: 10px; font-weight: 600;
    text-transform: uppercase;
  }
  .badge.running { background: #d29922; color: #0d1117; }
  .badge.queued { background: #30363d; color: #8b949e; }
  .badge.completed, .badge.done { background: #238636; color: #fff; }
  .badge.failed { background: #da3633; color: #fff; }
  .badge.suggested { background: #1f6feb; color: #fff; }
  .badge.approved { background: #d29922; color: #0d1117; }
  .badge.review { background: #a371f7; color: #fff; }
  .badge.in_progress { background: #d29922; color: #0d1117; }
  .badge.pending_review { background: #8957e5; color: #fff; }
  .badge.pending_qa { background: #8957e5; color: #fff; }
  .badge.rejected { background: #da3633; color: #fff; }
  .badge.high { background: #da3633; color: #fff; }
  .badge.normal { background: #30363d; color: #8b949e; }

  /* ── Activity Feed cards ───────────────────────── */
  .card {
    background: #161b22; border: 1px solid #30363d; border-radius: 8px;
    padding: 10px 14px; margin-bottom: 6px; display: flex; align-items: flex-start; gap: 10px;
  }
  .card .icon { font-size: 1.2em; min-width: 24px; text-align: center; }
  .card .body { flex: 1; }
  .card .agent { color: #58a6ff; font-weight: 600; font-size: 0.85em; }
  .card .msg { color: #c9d1d9; margin-top: 2px; font-size: 0.82em; }
  .card .time { color: #484f58; font-size: 0.72em; margin-top: 2px; }
  .card.completed { border-left: 3px solid #3fb950; }
  .card.running { border-left: 3px solid #d29922; }
  .card.queued { border-left: 3px solid #8b949e; }
  .card.failed { border-left: 3px solid #f85149; }
  .card.info { border-left: 3px solid #58a6ff; }
  .card .start-btn {
    background: #238636; color: #fff; border: none; border-radius: 4px;
    padding: 3px 10px; font-size: 0.72em; cursor: pointer; margin-left: 8px;
  }
  .card .start-btn:hover { background: #2ea043; }

  /* ── Filter bar ────────────────────────────────── */
  .filter-bar {
    display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; align-items: center;
  }
  .filter-bar label { color: #8b949e; font-size: 0.8em; margin-right: 4px; }
  .filter-bar select, .filter-bar input {
    background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
    border-radius: 6px; padding: 4px 10px; font-size: 0.82em; font-family: inherit;
  }

  /* ── History list (compact, not cards) ─────────── */
  .history-toggle {
    color: #58a6ff; cursor: pointer; font-size: 0.8em; display: flex;
    align-items: center; gap: 4px; user-select: none;
  }
  .history-toggle .chevron { transition: transform 0.2s; font-size: 0.7em; }
  .history-toggle .chevron.open { transform: rotate(90deg); }
  .history-list { display: none; margin-top: 8px; }
  .history-list.open { display: block; }
  .history-row {
    display: flex; align-items: center; gap: 8px; padding: 5px 8px;
    border-bottom: 1px solid #161b22; font-size: 0.8em;
  }
  .history-row:hover { background: #161b22; }
  .history-row .h-agent { color: #58a6ff; min-width: 70px; font-weight: 600; }
  .history-row .h-msg { color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .history-row .h-time { color: #484f58; font-size: 0.85em; min-width: 120px; text-align: right; }
  .history-row .h-detail {
    display: none; padding: 6px 8px 6px 78px; font-size: 0.78em; color: #8b949e;
    border-bottom: 1px solid #161b22; white-space: pre-wrap; word-break: break-word;
  }
  .history-row .h-detail.open { display: block; }
  .h-expand {
    cursor: pointer; color: #484f58; min-width: 16px; font-size: 0.7em;
    transition: transform 0.2s;
  }
  .h-expand.open { transform: rotate(90deg); color: #58a6ff; }

  /* ── Kanban Board ──────────────────────────────── */
  .kanban-toolbar {
    display: flex; align-items: center; gap: 10px; margin-bottom: 4px;
  }
  .kanban-toolbar .toggle-empty {
    background: none; border: 1px solid #30363d; color: #8b949e; border-radius: 6px;
    padding: 3px 10px; font-size: 0.72em; cursor: pointer; font-family: inherit;
    transition: all 0.15s;
  }
  .kanban-toolbar .toggle-empty:hover { border-color: #58a6ff; color: #58a6ff; }
  .kanban-toolbar .toggle-empty.active { background: #1f6feb22; border-color: #58a6ff; color: #58a6ff; }
  .kanban-board {
    display: flex;
    overflow-x: auto;
    gap: 14px; margin-top: 8px;
    padding-bottom: 12px;
    scroll-snap-type: x proximity;
  }
  .kanban-board::-webkit-scrollbar { height: 6px; }
  .kanban-board::-webkit-scrollbar-track { background: #0d1117; border-radius: 3px; }
  .kanban-board::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
  .kanban-board::-webkit-scrollbar-thumb:hover { background: #484f58; }
  .kanban-col {
    background: linear-gradient(180deg, #161b22 0%, #0d1117 100%);
    border: 1px solid #30363d; border-radius: 12px;
    padding: 16px; min-height: 180px;
    min-width: 220px; flex: 1 1 0;
    box-shadow: 0 2px 12px rgba(0,0,0,0.25);
    scroll-snap-align: start;
    transition: opacity 0.2s, flex 0.2s;
  }
  .kanban-col.hidden-empty { display: none; }
  .kanban-col.empty-col { opacity: 0.35; min-height: 80px; flex: 0.3 1 0; }
  /* When empty cols are hidden, remaining visible cols expand to fill */
  .kanban-board.hide-empties .kanban-col:not(.hidden-empty) { flex: 1 1 0; min-width: 280px; }
  .kanban-col.empty-col .kanban-empty-hint { display: block; }
  .kanban-empty-hint { display: none; color: #484f58; font-size: 0.75em; text-align: center; padding: 20px 8px; }

  /* Action cards (Walgreens-style notification banners) */
  .kanban-action-card {
    display: flex; align-items: center; gap: 14px;
    padding: 14px 18px; border-radius: 10px;
    font-size: 0.88em; line-height: 1.5;
  }
  .kanban-action-card.info {
    background: linear-gradient(135deg, #1f3a5f 0%, #161b22 100%);
    border: 1px solid #1f6feb44;
    color: #79c0ff;
  }
  .kanban-action-card.warning {
    background: linear-gradient(135deg, #3d2e00 0%, #161b22 100%);
    border: 1px solid #d2992244;
    color: #e3b341;
  }
  .kanban-action-card.success {
    background: linear-gradient(135deg, #0d3117 0%, #161b22 100%);
    border: 1px solid #3fb95044;
    color: #56d364;
  }
  .kanban-action-card .kac-icon { font-size: 1.4em; flex-shrink: 0; }
  .kanban-action-card .kac-body { flex: 1; }
  .kanban-action-card .kac-title { font-weight: 600; margin-bottom: 2px; }
  .kanban-action-card .kac-desc { font-size: 0.88em; opacity: 0.85; }
  .kanban-action-card .kac-action {
    background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
    color: inherit; padding: 5px 14px; border-radius: 6px; font-size: 0.82em;
    cursor: pointer; font-weight: 600; white-space: nowrap; font-family: inherit;
    transition: background 0.15s;
  }
  .kanban-action-card .kac-action:hover { background: rgba(255,255,255,0.18); }
  .kanban-action-card .kac-dismiss {
    background: none; border: none; color: inherit; opacity: 0.5;
    font-size: 1em; cursor: pointer; padding: 4px;
  }
  .kanban-action-card .kac-dismiss:hover { opacity: 1; }
  .kanban-col h3 {
    font-size: 0.82em; color: #8b949e; margin-bottom: 14px;
    padding-bottom: 10px; border-bottom: 2px solid #21262d;
    display: flex; justify-content: space-between; align-items: center;
    text-transform: uppercase; letter-spacing: 0.6px;
  }
  .kanban-col h3 .count {
    background: #21262d; color: #8b949e; border-radius: 10px;
    padding: 2px 10px; font-size: 0.82em; font-weight: 700;
    min-width: 20px; text-align: center;
    transition: background 0.2s, color 0.2s;
  }
  .kanban-col h3 .count.has-items { background: #1f6feb; color: #fff; }
  .k-card {
    background: #0d1117; border: 1px solid #21262d; border-radius: 10px;
    padding: 14px 16px; margin-bottom: 10px; font-size: 0.82em;
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
    transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
    position: relative;
  }
  .k-card:hover { border-color: #58a6ff; box-shadow: 0 4px 16px rgba(88,166,255,0.12); transform: translateY(-2px); }
  .k-card.priority-critical { border-left: 4px solid #f85149; }
  .k-card.priority-high { border-left: 4px solid #d29922; }
  .k-card.priority-normal { border-left: 4px solid #58a6ff; }
  .k-card.priority-low { border-left: 4px solid #8b949e; }
  .k-card .k-title { color: #e6edf3; font-weight: 600; margin-bottom: 8px; font-size: 0.9em; line-height: 1.35; }
  .k-card .k-agent { color: #7d8590; font-size: 0.76em; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .k-card .k-agent .badge {
    font-size: 0.68em; padding: 2px 8px; border-radius: 10px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.4px;
  }
  .k-card .k-agent .badge.critical { background: rgba(248,81,73,0.15); color: #f85149; }
  .k-card .k-agent .badge.high { background: rgba(210,153,34,0.15); color: #d29922; }
  .k-card .k-agent .badge.normal { background: rgba(88,166,255,0.1); color: #58a6ff; }
  .k-card .k-agent .badge.low { background: rgba(139,148,158,0.1); color: #8b949e; }
  .k-card .k-desc { color: #8b949e; font-size: 0.82em; margin-top: 8px; }
  .k-card .k-desc-text { max-height: 40px; overflow: hidden; transition: max-height 0.2s ease; }
  .k-card .k-desc-text.expanded { max-height: 600px; }
  .k-card .k-expand {
    color: #58a6ff; font-size: 0.72em; cursor: pointer; margin-top: 2px;
    display: inline-flex; align-items: center; gap: 3px; user-select: none;
  }
  .k-card .k-expand .chevron { transition: transform 0.2s; display: inline-block; font-size: 0.7em; }
  .k-card .k-expand .chevron.open { transform: rotate(90deg); }
  .k-card .k-time { color: #484f58; font-size: 0.72em; margin-top: 6px; }
  .k-card .k-result { color: #8b949e; font-size: 0.8em; margin-top: 8px; padding: 10px; background: #161b22; border-radius: 8px; max-height: 80px; overflow: auto; border: 1px solid #21262d; }
  .k-card .k-notes { color: #6e7681; font-size: 0.72em; margin-top: 4px; font-style: italic; }
  .k-card .k-actions { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
  .k-btn {
    border: none; border-radius: 8px; padding: 6px 14px; font-size: 0.76em;
    cursor: pointer; font-family: inherit; font-weight: 600;
    display: inline-flex; align-items: center; gap: 5px;
    transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
  }
  .k-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.2); }
  .k-btn:active { transform: translateY(0); }
  .k-btn.approve { background: #238636; color: #fff; }
  .k-btn.approve:hover { background: #2ea043; }
  .k-btn.reject { background: #da3633; color: #fff; }
  .k-btn.reject:hover { background: #f85149; }
  .k-btn.info { background: #1f6feb; color: #fff; }
  .k-btn.info:hover { background: #388bfd; }
  .k-btn.archive { background: #30363d; color: #8b949e; }
  .k-btn.archive:hover { background: #484f58; color: #c9d1d9; }
  .k-btn.assign { background: #1f3a5f; color: #58a6ff; }
  .k-btn.assign:hover { background: #264670; }

  /* ── Card Detail Modal ───────────────────────────── */
  .modal-overlay {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.6); z-index: 2000; display: none;
    justify-content: center; align-items: center;
  }
  .modal-overlay.open { display: flex; }
  .modal-card {
    background: #161b22; border: 1px solid #30363d; border-radius: 12px;
    padding: 24px; max-width: 640px; width: 90%; max-height: 80vh;
    overflow-y: auto; position: relative; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  }
  .modal-card .modal-close {
    position: absolute; top: 12px; right: 16px; background: none; border: none;
    color: #8b949e; font-size: 1.4em; cursor: pointer; padding: 4px 8px;
    border-radius: 4px;
  }
  .modal-card .modal-close:hover { color: #f0f6fc; background: #21262d; }
  .modal-card .modal-title {
    font-size: 1.2em; font-weight: 700; color: #f0f6fc; margin-bottom: 8px;
    padding-right: 40px;
  }
  .modal-card .modal-meta {
    display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px;
    font-size: 0.85em;
  }
  .modal-card .modal-meta .meta-agent { color: #58a6ff; font-weight: 600; }
  .modal-card .modal-meta .meta-category { color: #8b949e; }
  .modal-card .modal-meta .meta-status {
    padding: 1px 10px; border-radius: 10px; font-size: 0.85em; font-weight: 600;
  }
  .meta-status.pending_review { background: #30363d; color: #8b949e; }
  .meta-status.suggested { background: #1f2937; color: #58a6ff; }
  .meta-status.approved { background: #132a13; color: #3fb950; }
  .meta-status.in_progress { background: #2a1f00; color: #d29922; }
  .meta-status.pending_qa { background: #30363d; color: #8b949e; }
  .meta-status.review { background: #1f2937; color: #58a6ff; }
  .meta-status.done { background: #132a13; color: #3fb950; }
  .modal-card .modal-section {
    margin-bottom: 14px;
  }
  .modal-card .modal-section-label {
    font-size: 0.75em; color: #484f58; text-transform: uppercase;
    letter-spacing: 0.05em; margin-bottom: 4px; font-weight: 600;
  }
  .modal-card .modal-section-body {
    color: #c9d1d9; font-size: 0.9em; line-height: 1.5;
    white-space: pre-wrap; word-break: break-word;
  }
  .modal-card .modal-result {
    background: #0d1117; border: 1px solid #21262d; border-radius: 6px;
    padding: 12px; color: #c9d1d9; font-size: 0.85em; line-height: 1.5;
    white-space: pre-wrap; word-break: break-word; max-height: 300px;
    overflow-y: auto;
  }
  .modal-card .modal-actions {
    margin-top: 18px; display: flex; gap: 8px; flex-wrap: wrap;
    padding-top: 14px; border-top: 1px solid #21262d;
  }

  /* ── Slide-out drawer ──────────────────────────── */
  .drawer-toggle {
    position: fixed; top: 50%; right: 0; transform: translateY(-50%);
    background: #161b22; border: 1px solid #30363d; border-right: none;
    color: #58a6ff; padding: 12px 8px; cursor: pointer;
    border-radius: 8px 0 0 8px; font-size: 1.1em; z-index: 1001;
    writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: 2px;
  }
  .drawer-toggle:hover { background: #21262d; }
  .drawer-overlay {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.5); z-index: 1002; display: none;
  }
  .drawer-overlay.open { display: block; }
  .drawer {
    position: fixed; top: 0; right: -440px; width: 420px; height: 100%;
    background: #0d1117; border-left: 1px solid #30363d;
    z-index: 1003; transition: right 0.25s ease; overflow-y: auto; padding: 20px;
  }
  .drawer.open { right: 0; }
  .drawer h2 { color: #58a6ff; font-size: 1.2em; margin-bottom: 4px; }
  .drawer .close-btn {
    position: absolute; top: 12px; right: 16px; background: none; border: none;
    color: #8b949e; font-size: 1.4em; cursor: pointer;
  }
  .drawer .close-btn:hover { color: #c9d1d9; }
  .drawer-filter { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
  .drawer-filter input {
    background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
    border-radius: 6px; padding: 6px 12px; font-size: 0.85em; flex: 1; font-family: inherit;
  }
  .topic-pills { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 16px; }
  .topic-pill {
    background: #21262d; color: #8b949e; border: 1px solid #30363d;
    border-radius: 12px; padding: 3px 10px; font-size: 0.72em; cursor: pointer;
    transition: all 0.15s;
  }
  .topic-pill:hover, .topic-pill.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
  .drawer-section { margin: 16px 0; }
  .drawer-section h3 { color: #c9d1d9; font-size: 0.95em; margin-bottom: 8px; border-bottom: 1px solid #21262d; padding-bottom: 4px; }
  .cmd-item { display: flex; gap: 8px; padding: 6px 0; border-bottom: 1px solid #161b22; font-size: 0.82em; }
  .cmd-item.hidden { display: none; }
  .cmd-item code { color: #58a6ff; font-family: 'Consolas', monospace; white-space: nowrap; min-width: 140px; }
  .cmd-item span { color: #8b949e; }
  .agent-card-mini {
    background: #161b22; border: 1px solid #30363d; border-radius: 6px;
    padding: 8px 12px; margin-bottom: 6px; font-size: 0.82em;
  }
  .agent-card-mini.hidden { display: none; }
  .agent-card-mini .a-name { color: #58a6ff; font-weight: 600; }
  .agent-card-mini .a-title { color: #8b949e; margin-left: 8px; }
  .agent-card-mini .a-topics { margin-top: 3px; }
  .agent-card-mini .a-topics span {
    background: #21262d; color: #8b949e; border-radius: 8px;
    padding: 1px 6px; font-size: 0.75em; margin-right: 4px;
  }

  /* Toast notification */
  .toast {
    position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
    background: #238636; color: #fff; padding: 8px 20px; border-radius: 8px;
    font-size: 0.85em; opacity: 0; transition: opacity 0.3s; z-index: 2000;
    pointer-events: none;
  }
  .toast.show { opacity: 1; }
  @keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>

<!-- Tab bar -->
<div class="tab-bar">
  <button class="tab-btn active" onclick="switchTab('kanban')">Kanban Board</button>
  <button class="tab-btn" onclick="switchTab('feed')">Activity Feed</button>
</div>

<!-- ═══════════════════ ACTIVITY FEED TAB ═══════════════════ -->
<div class="tab-content" id="tab-feed">
  <h1>Activity Feed</h1>
  <p class="subtitle">Live polling every 5s &mdash; completed tasks move to history</p>

  <div class="filter-bar">
    <label>Agent:</label>
    <select id="agentFilter" onchange="applyFeedFilter()">
      <option value="">All agents</option>
      __AGENT_OPTIONS__
    </select>
    <label>Status:</label>
    <select id="statusFilter" onchange="applyFeedFilter()">
      <option value="">All</option>
      <option value="running">Running</option>
      <option value="completed">Completed</option>
      <option value="failed">Failed</option>
      <option value="info">Info</option>
      <option value="queued">Queued</option>
    </select>
  </div>

  <div class="section-title">Active Tasks</div>
  <div id="tasks"><div class="empty">Loading...</div></div>

  <div class="section-title">Recent Notifications</div>
  <div id="notifications"><div class="empty">Loading...</div></div>

  <div class="section-title">
    <span class="history-toggle" onclick="toggleHistory()">
      <span class="chevron" id="historyChevron">&#9654;</span> Task History
    </span>
  </div>
  <div class="history-list" id="historyList"></div>
</div>

<!-- ═══════════════════ KANBAN BOARD TAB ═══════════════════ -->
<div class="tab-content active" id="tab-kanban">
  <h1>Kanban Board</h1>
  <p class="subtitle">Agent-suggested tasks &mdash; approve to start, review results</p>

  <!-- Action cards / notification banner area -->
  <div id="kanbanActionCards" style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px"></div>

  <!-- Board tabs -->
  <div style="display:flex;gap:0;margin-bottom:12px;border-bottom:1px solid #30363d" id="boardTabs">
    <button class="tab-btn active" onclick="switchBoard('')" data-board="" style="font-size:0.82em;padding:8px 16px">All Boards</button>
    __BOARD_TABS__
  </div>

  <!-- Filters + guided mode toggle -->
  <div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center">
    <input type="text" id="kanbanSearch" placeholder="Search by ID or title..." oninput="applyKanbanFilters()" style="background:#161b22;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85em;width:180px">
    <select id="kanbanFilterAgent" onchange="applyKanbanFilters()" style="background:#161b22;border:1px solid #30363d;color:#c9d1d9;padding:6px 8px;border-radius:6px;font-size:0.85em">
      <option value="">All Agents</option>
    </select>
    <select id="kanbanFilterPriority" onchange="applyKanbanFilters()" style="background:#161b22;border:1px solid #30363d;color:#c9d1d9;padding:6px 8px;border-radius:6px;font-size:0.85em">
      <option value="">All Priorities</option>
      <option value="high">High</option>
      <option value="normal">Normal</option>
      <option value="low">Low</option>
    </select>
    <button onclick="clearKanbanFilters()" style="background:transparent;border:1px solid #30363d;color:#8b949e;padding:6px 10px;border-radius:6px;font-size:0.85em;cursor:pointer">Clear</button>
    <span style="flex:1"></span>
    <button class="toggle-empty" id="guidedModeBtn" onclick="toggleGuidedMode()" style="background:none;border:1px solid #30363d;color:#8b949e;border-radius:6px;padding:4px 10px;font-size:0.72em;cursor:pointer">&#x1F9ED; Guided</button>
    <button class="toggle-empty" id="toggleEmptyBtn" onclick="toggleEmptyCols()" style="background:none;border:1px solid #30363d;color:#8b949e;border-radius:6px;padding:4px 10px;font-size:0.72em;cursor:pointer">Show empty columns</button>
  </div>

  <!-- Guided mode panel (step-by-step walkthrough) -->
  <div id="guidedPanel" style="display:none;margin-bottom:16px;background:#161b22;border:1px solid #1f6feb44;border-radius:12px;padding:20px;position:relative">
    <button onclick="toggleGuidedMode()" style="position:absolute;top:10px;right:12px;background:none;border:none;color:#8b949e;font-size:1.1em;cursor:pointer">&times;</button>
    <div id="guidedContent"></div>
    <div style="display:flex;gap:8px;margin-top:14px">
      <button id="guidedPrev" onclick="guidedStep(-1)" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 16px;border-radius:6px;font-size:0.82em;cursor:pointer">&larr; Previous</button>
      <button id="guidedNext" onclick="guidedStep(1)" style="background:#1f6feb;border:none;color:#fff;padding:6px 16px;border-radius:6px;font-size:0.82em;cursor:pointer;font-weight:600">Next &rarr;</button>
      <span style="flex:1"></span>
      <span id="guidedCounter" style="font-size:0.78em;color:#8b949e;align-self:center"></span>
    </div>
  </div>

  <div class="kanban-board" id="kanbanBoard">
    <div class="kanban-col" id="col-pending_review">
      <h3>Mgr Review <span class="count" id="cnt-pending_review">0</span></h3>
      <div id="cards-pending_review"></div>
    </div>
    <div class="kanban-col" id="col-suggested">
      <h3>Suggested <span class="count" id="cnt-suggested">0</span></h3>
      <div id="cards-suggested"></div>
    </div>
    <div class="kanban-col" id="col-approved">
      <h3>Approved <span class="count" id="cnt-approved">0</span></h3>
      <div id="cards-approved"></div>
    </div>
    <div class="kanban-col" id="col-in_progress">
      <h3>In Progress <span class="count" id="cnt-in_progress">0</span></h3>
      <div id="cards-in_progress"></div>
    </div>
    <div class="kanban-col" id="col-pending_qa">
      <h3>Mgr QA <span class="count" id="cnt-pending_qa">0</span></h3>
      <div id="cards-pending_qa"></div>
    </div>
    <div class="kanban-col" id="col-review">
      <h3>Your Review <span class="count" id="cnt-review">0</span></h3>
      <div id="cards-review"></div>
    </div>
    <div class="kanban-col" id="col-done">
      <h3>Done <span class="count" id="cnt-done">0</span></h3>
      <div id="cards-done"></div>
    </div>
  </div>
</div>

<!-- Card Detail Modal -->
<div class="modal-overlay" id="cardModal" onclick="closeModal(event)">
  <div class="modal-card" id="modalContent">
    <button class="modal-close" onclick="closeCardModal()">&times;</button>
    <div class="modal-title" id="modalTitle"></div>
    <div class="modal-meta" id="modalMeta"></div>
    <div id="modalBody"></div>
    <div class="modal-actions" id="modalActions"></div>
  </div>
</div>

<!-- Drawer toggle -->
<div class="drawer-toggle" onclick="toggleDrawer()">&#x2630; Help</div>
<div class="drawer-overlay" id="drawerOverlay" onclick="toggleDrawer()"></div>
<div class="drawer" id="helpDrawer">
  <button class="close-btn" onclick="toggleDrawer()">&times;</button>
  <h2>Command Reference</h2>
  <p class="subtitle">Filter by agent name or topic</p>
  <div class="drawer-filter">
    <input type="text" id="drawerSearch" placeholder="Search commands or agents..." oninput="filterDrawer()">
  </div>
  <div class="topic-pills" id="topicPills">__TOPIC_PILLS__</div>
  <div class="drawer-section">
    <h3>CLI Commands</h3>
    <div id="cmdList">__CMD_LIST__</div>
  </div>
  <div class="drawer-section">
    <h3>Agent Roster</h3>
    <div id="agentList">__AGENT_LIST__</div>
  </div>
</div>

<!-- Toast -->
<div class="toast" id="toast"></div>

<script>
// ── Tab switching ────────────────────────────────────
function switchTab(tab) {
  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
  document.querySelector(`[onclick="switchTab('${tab}')"]`).classList.add('active');
  document.getElementById(`tab-${tab}`).classList.add('active');
  try { localStorage.setItem('vastian_active_tab', tab); } catch(e) {}
}
// Restore last active tab on page load (URL ?tab= overrides localStorage)
try {
  const params = new URLSearchParams(window.location.search);
  const urlTab = params.get('tab');
  if (urlTab && document.getElementById(`tab-${urlTab}`)) {
    switchTab(urlTab);
  } else {
    const savedTab = localStorage.getItem('vastian_active_tab');
    if (savedTab && document.getElementById(`tab-${savedTab}`)) switchTab(savedTab);
  }
} catch(e) {}

// ── Toast ──────────────────────────────────────────
function showToast(msg) {
  const t = document.getElementById('toast');
  t.textContent = msg;
  t.classList.add('show');
  setTimeout(() => t.classList.remove('show'), 2000);
}

// ── Live polling ──────────────────────────────────────
let lastData = null;
let historyData = null;

async function poll() {
  try {
    const boardQ = activeBoard ? `?board=${activeBoard}` : '';
    const [feedResp, kanbanResp] = await Promise.all([
      fetch('/api/notifications'),
      fetch('/api/kanban' + boardQ),
    ]);
    const feedData = await feedResp.json();
    const kanbanData = await kanbanResp.json();
    lastData = feedData;
    renderFeed(feedData);
    renderKanban(kanbanData);
  } catch(e) { /* server may be restarting */ }
}

async function loadHistory() {
  try {
    const resp = await fetch('/api/history');
    historyData = await resp.json();
    renderHistory(historyData);
  } catch(e) {}
}

const ICONS = {completed:'&#x2705;',failed:'&#x274C;',queued:'&#x1F4E5;',info:'&#x1F4AC;',running:'&#x26A1;'};

function renderFeed(data) {
  const af = document.getElementById('agentFilter').value;
  const sf = document.getElementById('statusFilter').value;

  // Active tasks (only queued/running)
  const tasksEl = document.getElementById('tasks');
  let tasks = data.tasks || [];
  if (af) tasks = tasks.filter(t => t.agent_id === af);
  if (tasks.length) {
    tasksEl.innerHTML = tasks.map(t => {
      const icon = t.status === 'running' ? '&#x23F3;' : '&#x1F4CB;';
      const startBtn = t.status === 'queued'
        ? `<button class="start-btn" onclick="forceStart('${t.task_id}')">&#x25B6; Start</button>`
        : '';
      return `<div class="card ${t.status}"><div class="icon">${icon}</div><div class="body"><div style="display:flex;justify-content:space-between;align-items:center"><span class="agent">${t.agent_id}</span><span><span class="badge ${t.status}">${t.status}</span>${startBtn}</span></div><div class="msg">${esc(t.description)}</div><div class="time">${(t.created_at||'').slice(0,19)}</div></div></div>`;
    }).join('');
  } else {
    tasksEl.innerHTML = '<div class="empty">No active tasks</div>';
  }

  // Notifications (only recent non-completed)
  const notifEl = document.getElementById('notifications');
  let notifs = (data.notifications || []).slice(0, 20);
  if (af) notifs = notifs.filter(n => n.agent_id === af);
  if (sf) notifs = notifs.filter(n => n.type === sf);
  if (notifs.length) {
    notifEl.innerHTML = notifs.map(n => {
      const icon = ICONS[n.type] || ICONS.info;
      return `<div class="card ${n.type}"><div class="icon">${icon}</div><div class="body"><span class="agent">${n.agent_id}</span><div class="msg">${esc(n.message)}</div><div class="time">${(n.created_at||'').slice(0,19)}</div></div></div>`;
    }).join('');
  } else {
    notifEl.innerHTML = '<div class="empty">No notifications yet</div>';
  }
}

function applyFeedFilter() { if (lastData) renderFeed(lastData); }

// ── History (compact list with chevron expand) ────────
let historyOpen = false;

function toggleHistory() {
  historyOpen = !historyOpen;
  document.getElementById('historyChevron').classList.toggle('open', historyOpen);
  document.getElementById('historyList').classList.toggle('open', historyOpen);
  if (historyOpen && !historyData) loadHistory();
}

function renderHistory(data) {
  const el = document.getElementById('historyList');
  const items = [...(data.tasks || []), ...(data.notifications || [])];
  items.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));

  if (!items.length) {
    el.innerHTML = '<div class="empty">No history yet</div>';
    return;
  }

  el.innerHTML = items.slice(0, 100).map((item, i) => {
    const agent = item.agent_id || '?';
    const msg = item.description || item.message || '';
    const time = (item.created_at || '').slice(0, 19);
    const detail = item.result || item.message || '';
    return `<div class="history-row" onclick="toggleHistoryDetail(${i})"><span class="h-expand" id="hexp-${i}">&#9654;</span><span class="h-agent">${esc(agent)}</span><span class="h-msg">${esc(msg)}</span><span class="h-time">${time}</span></div><div class="history-row h-detail" id="hdet-${i}">${esc(detail)}</div>`;
  }).join('');
}

function toggleHistoryDetail(i) {
  const det = document.getElementById('hdet-' + i);
  const exp = document.getElementById('hexp-' + i);
  det.classList.toggle('open');
  exp.classList.toggle('open');
}

// ── Board switching ───────────────────────────────────
let activeBoard = '';
const BOARD_LABELS = __BOARD_LABELS__;
const BOARD_COLORS = __BOARD_COLORS__;

function switchBoard(board) {
  activeBoard = board;
  document.querySelectorAll('#boardTabs .tab-btn').forEach(b => {
    b.classList.toggle('active', (b.dataset.board || '') === board);
  });
  poll();
}

// ── Kanban Board ──────────────────────────────────────
let kanbanTaskCache = {};
let kanbanAllTasks = [];

function renderKanban(data) {
  kanbanAllTasks = (data.tasks || []).filter(t => t.status !== 'archived' && t.status !== 'rejected');
  kanbanTaskCache = {};
  kanbanAllTasks.forEach(t => { kanbanTaskCache[t.kanban_id] = t; });

  // Populate agent filter dropdown
  const agentSet = new Set(kanbanAllTasks.map(t => t.suggested_by).filter(Boolean));
  const agentSel = document.getElementById('kanbanFilterAgent');
  const curAgent = agentSel.value;
  agentSel.innerHTML = '<option value="">All Agents</option>' +
    [...agentSet].sort().map(a => `<option value="${a}"${a===curAgent?' selected':''}>${a}</option>`).join('');

  applyKanbanFilters();
}

function applyKanbanFilters() {
  const search = (document.getElementById('kanbanSearch').value || '').toLowerCase();
  const agentFilter = document.getElementById('kanbanFilterAgent').value;
  const prioFilter = document.getElementById('kanbanFilterPriority').value;

  const filtered = kanbanAllTasks.filter(t => {
    if (search && !t.kanban_id.toLowerCase().includes(search) && !t.title.toLowerCase().includes(search)) return false;
    if (agentFilter && t.suggested_by !== agentFilter) return false;
    if (prioFilter && t.priority !== prioFilter) return false;
    return true;
  });

  const cols = {pending_review: [], suggested: [], approved: [], in_progress: [], pending_qa: [], review: [], done: []};
  filtered.forEach(t => { if (cols[t.status]) cols[t.status].push(t); });

  var _showEmptyCols = localStorage.getItem('kanban-show-empty') === 'true';
  for (const [status, tasks] of Object.entries(cols)) {
    const cntEl = document.getElementById('cnt-' + status);
    const container = document.getElementById('cards-' + status);
    const colEl = document.getElementById('col-' + status);
    if (!cntEl || !container) continue;
    cntEl.textContent = tasks.length;
    if (tasks.length > 0) {
      cntEl.classList.add('has-items');
    } else {
      cntEl.classList.remove('has-items');
    }
    if (colEl) {
      if (!tasks.length) {
        colEl.classList.add('empty-col');
        if (!_showEmptyCols) colEl.classList.add('hidden-empty');
        else colEl.classList.remove('hidden-empty');
      } else {
        colEl.classList.remove('empty-col', 'hidden-empty');
      }
    }
    if (!tasks.length) {
      container.innerHTML = '<div class="kanban-empty-hint">No cards</div>';
      continue;
    }
    container.innerHTML = tasks.map(t => {
      let actions = '';
      if (status === 'suggested') {
        actions = `<button class="k-btn approve" onclick="event.stopPropagation();kanbanAction('approve-start','${t.kanban_id}')">&#x2713; Approve</button><button class="k-btn assign" onclick="event.stopPropagation();kanbanAction('assign-me','${t.kanban_id}')">&#x1F464; Assign</button><button class="k-btn reject" onclick="event.stopPropagation();rejectSuggestion('${t.kanban_id}')">&#x2715; Reject</button>`;
      } else if (status === 'review') {
        actions = `<button class="k-btn approve" onclick="event.stopPropagation();kanbanAction('approve-result','${t.kanban_id}')">&#x2713; Accept</button><button class="k-btn reject" onclick="event.stopPropagation();kanbanReject('${t.kanban_id}')">&#x21BA; Redo</button>`;
      } else if (status === 'pending_review') {
        actions = `<span style="color:#8b949e;font-size:0.75em;display:flex;align-items:center;gap:4px"><span class="spin" style="display:inline-block;width:10px;height:10px;border:2px solid #8b949e;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite"></span> Manager reviewing...</span>`;
      } else if (status === 'pending_qa') {
        actions = `<span style="color:#8b949e;font-size:0.75em;display:flex;align-items:center;gap:4px"><span class="spin" style="display:inline-block;width:10px;height:10px;border:2px solid #8b949e;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite"></span> QA in progress...</span>`;
      } else if (status === 'done') {
        actions = `<button class="k-btn archive" onclick="event.stopPropagation();archiveTask('${t.kanban_id}')">&#x1F4E6; Archive</button>`;
      }
      const descFull = esc(t.description);
      const assignBadge = t.assigned_to === 'user' ? ' <span style="background:#1f3a5f;color:#58a6ff;padding:2px 8px;border-radius:6px;font-size:0.7em;font-weight:600">YOURS</span>' : '';
      const boardColor = BOARD_COLORS[t.board] || '#8b949e';
      const boardLabel = BOARD_LABELS[t.board] || (t.board ? t.board.replace(/-/g,' ').toUpperCase() : '');
      const boardBadge = boardLabel ? `<span style="background:${boardColor}18;color:${boardColor};padding:2px 8px;border-radius:6px;font-size:0.68em;font-weight:600;margin-right:4px">${boardLabel}</span>` : '';
      const prioClass = 'priority-' + (t.priority || 'normal');
      const hasDetails = t.description || (t.tags && t.tags.length) || t.user_notes;
      const tagsHtml = (t.tags && t.tags.length) ? '<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">' + t.tags.map(tg => `<span style="background:#21262d;color:#8b949e;padding:2px 8px;border-radius:8px;font-size:0.68em">${esc(tg)}</span>`).join('') + '</div>' : '';
      const notesHtml = t.user_notes ? `<div class="k-notes">${esc(t.user_notes)}</div>` : '';
      const ticketDisplay = t.ticket_id || t.kanban_id;
      const detailsBlock = hasDetails
        ? `<div class="k-details" style="display:none;margin-top:8px;padding-top:8px;border-top:1px solid #21262d"><div class="k-desc" style="font-size:0.82em">${descFull}</div>${tagsHtml}${notesHtml}<div class="k-time"><span style="color:#6e7681;user-select:all;font-size:0.72em">${ticketDisplay}</span> <span style="font-size:0.72em;color:#484f58">${(t.created_at||'').slice(0,10)}</span></div></div>`
        : '';
      const toggleBtn = hasDetails
        ? `<span class="k-expand" onclick="event.stopPropagation();var d=this.parentElement.querySelector('.k-details');if(d){d.style.display=d.style.display==='none'?'block':'none';this.querySelector('.chevron').classList.toggle('open')}"><span class="chevron">&#9654;</span></span>`
        : '';
      return `<div class="k-card ${prioClass}" data-board="${esc(t.board||'')}" onclick="openCardModal('${t.kanban_id}')"><div class="k-title">${boardBadge}${esc(t.title)} ${toggleBtn}</div><div class="k-agent">${esc(t.suggested_by)}${assignBadge} <span class="badge ${t.priority}">${t.priority}</span></div>${detailsBlock}<div class="k-actions">${actions}</div></div>`;
    }).join('');
  }
  /* Refresh action cards after filter */
  renderKanbanActionCards();
}

function clearKanbanFilters() {
  document.getElementById('kanbanSearch').value = '';
  document.getElementById('kanbanFilterAgent').value = '';
  document.getElementById('kanbanFilterPriority').value = '';
  applyKanbanFilters();
}

function toggleEmptyCols() {
  var show = localStorage.getItem('kanban-show-empty') !== 'true';
  localStorage.setItem('kanban-show-empty', show ? 'true' : 'false');
  var btn = document.getElementById('toggleEmptyBtn');
  if (btn) { btn.textContent = show ? 'Hide empty columns' : 'Show empty columns'; btn.classList.toggle('active', show); }
  /* Toggle board class so remaining cols expand */
  var board = document.querySelector('.kanban-board');
  if (board) { board.classList.toggle('hide-empties', !show); }
  applyKanbanFilters();
}
(function() {
  var show = localStorage.getItem('kanban-show-empty') === 'true';
  var btn = document.getElementById('toggleEmptyBtn');
  if (btn && show) { btn.textContent = 'Hide empty columns'; btn.classList.add('active'); }
  /* Apply hide-empties class if not showing empty cols */
  var board = document.querySelector('.kanban-board');
  if (board && !show) { board.classList.add('hide-empties'); }
})();

/* ── Action cards (Walgreens-style notification banners) ── */
function renderKanbanActionCards() {
  var container = document.getElementById('kanbanActionCards');
  if (!container) return;
  var dismissed = JSON.parse(localStorage.getItem('kanban-dismissed-cards') || '{}');
  var cards = [];

  /* Check for items needing review */
  var reviewCount = 0;
  var suggestedCount = 0;
  var pendingReview = 0;
  kanbanAllTasks.forEach(function(t) {
    if (t.status === 'review') reviewCount++;
    if (t.status === 'suggested') suggestedCount++;
    if (t.status === 'pending_review') pendingReview++;
  });

  if (reviewCount > 0 && !dismissed['review-' + reviewCount]) {
    cards.push({id: 'review-' + reviewCount, type: 'warning', icon: '&#x1F50D;', title: reviewCount + ' task' + (reviewCount > 1 ? 's' : '') + ' ready for your review', desc: 'Your agents completed work that needs your approval before it can be finalized.', action: 'Review Now', actionFn: function() { document.getElementById('col-review').scrollIntoView({behavior:'smooth',block:'nearest',inline:'start'}); }});
  }
  if (suggestedCount > 0 && !dismissed['suggested-' + suggestedCount]) {
    cards.push({id: 'suggested-' + suggestedCount, type: 'info', icon: '&#x1F4A1;', title: suggestedCount + ' new suggestion' + (suggestedCount > 1 ? 's' : '') + ' from your agents', desc: 'Your team has proposed new tasks. Approve the ones you want to start.', action: 'View Suggestions', actionFn: function() { document.getElementById('col-suggested').scrollIntoView({behavior:'smooth',block:'nearest',inline:'start'}); }});
  }
  if (pendingReview > 0 && !dismissed['pending-' + pendingReview]) {
    cards.push({id: 'pending-' + pendingReview, type: 'info', icon: '&#x23F3;', title: pendingReview + ' task' + (pendingReview > 1 ? 's' : '') + ' under manager review', desc: 'Liam is reviewing these tasks. You\'ll be notified when they\'re ready.', action: null, actionFn: null});
  }
  if (!kanbanAllTasks.length) {
    cards.push({id: 'onboard-empty', type: 'info', icon: '&#x1F44B;', title: 'Your board is empty', desc: 'Talk to Charles in the chat to get your first tasks. Try saying "suggest some tasks" or run /check-in.', action: 'Open Chat', actionFn: function() { var inp = document.getElementById('chatInput'); if(inp) inp.focus(); }});
  }

  if (!cards.length) { container.innerHTML = ''; return; }
  var html = '';
  cards.forEach(function(c) {
    if (dismissed[c.id]) return;
    html += '<div class="kanban-action-card ' + c.type + '">';
    html += '<span class="kac-icon">' + c.icon + '</span>';
    html += '<div class="kac-body"><div class="kac-title">' + c.title + '</div><div class="kac-desc">' + c.desc + '</div></div>';
    if (c.action) html += '<button class="kac-action" data-card-id="' + c.id + '">' + c.action + '</button>';
    html += '<button class="kac-dismiss" data-dismiss-id="' + c.id + '" title="Dismiss">&times;</button>';
    html += '</div>';
  });
  container.innerHTML = html;
  /* Bind actions */
  cards.forEach(function(c) {
    if (c.actionFn) {
      var btn = container.querySelector('[data-card-id="' + c.id + '"]');
      if (btn) btn.addEventListener('click', c.actionFn);
    }
  });
  container.querySelectorAll('.kac-dismiss').forEach(function(btn) {
    btn.addEventListener('click', function() {
      var id = btn.dataset.dismissId;
      dismissed[id] = true;
      localStorage.setItem('kanban-dismissed-cards', JSON.stringify(dismissed));
      btn.parentElement.remove();
    });
  });
}

/* ── Guided Mode (step-by-step Kanban walkthrough) ── */
var _guidedActive = false;
var _guidedIdx = 0;
var _guidedSteps = [];

function toggleGuidedMode() {
  _guidedActive = !_guidedActive;
  var panel = document.getElementById('guidedPanel');
  var btn = document.getElementById('guidedModeBtn');
  if (!panel) return;
  if (_guidedActive) {
    panel.style.display = 'block';
    if (btn) { btn.classList.add('active'); btn.innerHTML = '&#x1F9ED; Guided (on)'; }
    _buildGuidedSteps();
    _guidedIdx = 0;
    _renderGuidedStep();
  } else {
    panel.style.display = 'none';
    if (btn) { btn.classList.remove('active'); btn.innerHTML = '&#x1F9ED; Guided'; }
    /* Un-highlight columns */
    document.querySelectorAll('.kanban-col').forEach(function(c) { c.style.outline = ''; c.style.outlineOffset = ''; });
  }
}

function _buildGuidedSteps() {
  _guidedSteps = [];
  var statusLabels = {
    pending_review: {label: 'Manager Review', desc: 'Tasks here are being reviewed by your project manager (Liam) before they reach you. No action needed.', icon: '&#x23F3;'},
    suggested: {label: 'Suggested', desc: 'Your agents have proposed these tasks. <strong>Approve</strong> the ones you want to start, <strong>reject</strong> the ones you don\'t. This is your main decision point.', icon: '&#x1F4A1;'},
    approved: {label: 'Approved', desc: 'You approved these tasks and agents are preparing to work on them. They\'ll move to In Progress shortly.', icon: '&#x2705;'},
    in_progress: {label: 'In Progress', desc: 'Agents are actively working on these tasks right now. You\'ll be notified when they finish.', icon: '&#x1F3C3;'},
    pending_qa: {label: 'Manager QA', desc: 'Completed tasks go through quality assurance by Theo before they reach you. This ensures high-quality output.', icon: '&#x1F50D;'},
    review: {label: 'Your Review', desc: 'These tasks are done and need your final review. <strong>Accept</strong> if the work is good, or <strong>Redo</strong> if it needs changes.', icon: '&#x2757;'},
    done: {label: 'Done', desc: 'Completed and approved tasks. You can archive these when you\'re ready to clear the board.', icon: '&#x2714;'}
  };
  var statuses = ['suggested', 'pending_review', 'approved', 'in_progress', 'pending_qa', 'review', 'done'];
  statuses.forEach(function(s) {
    var info = statusLabels[s];
    var col = document.getElementById('col-' + s);
    var count = 0;
    kanbanAllTasks.forEach(function(t) { if (t.status === s) count++; });
    _guidedSteps.push({status: s, label: info.label, desc: info.desc, icon: info.icon, count: count, colEl: col});
  });
}

function _renderGuidedStep() {
  var content = document.getElementById('guidedContent');
  var counter = document.getElementById('guidedCounter');
  var prevBtn = document.getElementById('guidedPrev');
  if (!content || !_guidedSteps.length) return;
  var step = _guidedSteps[_guidedIdx];
  var html = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">';
  html += '<span style="font-size:1.5em">' + step.icon + '</span>';
  html += '<div><div style="font-weight:700;font-size:1.05em;color:#c9d1d9">' + step.label + '</div>';
  html += '<span style="font-size:0.78em;color:#8b949e">' + step.count + ' task' + (step.count !== 1 ? 's' : '') + ' here</span></div>';
  html += '</div>';
  html += '<div style="font-size:0.88em;color:#8b949e;line-height:1.6">' + step.desc + '</div>';
  content.innerHTML = html;
  if (counter) counter.textContent = (_guidedIdx + 1) + ' / ' + _guidedSteps.length;
  if (prevBtn) prevBtn.style.visibility = _guidedIdx === 0 ? 'hidden' : 'visible';
  /* Highlight the column */
  document.querySelectorAll('.kanban-col').forEach(function(c) { c.style.outline = ''; c.style.outlineOffset = ''; });
  if (step.colEl) {
    step.colEl.style.outline = '2px solid #1f6feb';
    step.colEl.style.outlineOffset = '3px';
    step.colEl.scrollIntoView({behavior: 'smooth', block: 'nearest', inline: 'center'});
  }
}

function guidedStep(dir) {
  _guidedIdx = Math.max(0, Math.min(_guidedSteps.length - 1, _guidedIdx + dir));
  _renderGuidedStep();
}

/* Run action cards on load */
setTimeout(renderKanbanActionCards, 200);

function toggleDesc(el) {
  const textEl = el.parentElement.querySelector('.k-desc-text');
  const chevron = el.querySelector('.chevron');
  if (textEl.classList.toggle('expanded')) {
    chevron.classList.add('open');
    el.lastChild.textContent = ' less';
  } else {
    chevron.classList.remove('open');
    el.lastChild.textContent = ' more';
  }
}

// ── Card Detail Modal ─────────────────────────────────
function openCardModal(kanbanId) {
  const t = kanbanTaskCache[kanbanId];
  if (!t) return;
  document.getElementById('modalTitle').textContent = t.title;

  // Meta badges
  const statusLabel = {pending_review:'Mgr Review',suggested:'Suggested',approved:'Approved',in_progress:'In Progress',pending_qa:'Mgr QA',review:'Your Review',done:'Done'};
  const mBoardColor = BOARD_COLORS[t.board] || '#8b949e';
  const mBoardLabel = t.board ? t.board.toUpperCase() : '';
  const mBoardBadge = mBoardLabel ? `<span style="background:${mBoardColor}22;color:${mBoardColor};padding:2px 8px;border-radius:4px;font-size:0.8em;font-weight:600">${mBoardLabel}</span>` : '';
  const ticketId = t.ticket_id || t.kanban_id;
  document.getElementById('modalMeta').innerHTML =
    mBoardBadge +
    `<span class="meta-agent">${esc(t.suggested_by)}</span>` +
    `<span class="meta-category">${esc(t.category)} &middot; ${t.priority}</span>` +
    `<span class="meta-status ${t.status}">${statusLabel[t.status] || t.status}</span>` +
    `<span style="color:#6e7681;font-size:0.8em;user-select:all;margin-left:8px">${ticketId}</span>`;

  // Body sections
  let body = '';
  body += `<div class="modal-section"><div class="modal-section-label">Description</div><div class="modal-section-body">${esc(t.description)}</div></div>`;
  if (t.user_notes) {
    body += `<div class="modal-section"><div class="modal-section-label">Notes / Feedback</div><div class="modal-section-body" style="color:#d29922">${esc(t.user_notes)}</div></div>`;
  }
  if (t.result) {
    body += `<div class="modal-section"><div class="modal-section-label">Result</div><div class="modal-result">${esc(t.result)}</div></div>`;
  }
  if (t.assigned_to && t.assigned_to !== t.suggested_by) {
    body += `<div class="modal-section"><div class="modal-section-label">Assigned To</div><div class="modal-section-body">${esc(t.assigned_to)}</div></div>`;
  }
  const times = [];
  if (t.created_at) times.push('Created: ' + t.created_at.slice(0,19));
  if (times.length) {
    body += `<div class="modal-section"><div class="modal-section-label">Timeline</div><div class="modal-section-body" style="color:#484f58;font-size:0.82em">${times.join(' &middot; ')}</div></div>`;
  }
  // Inbox linking hint
  body += `<div class="modal-section"><div class="modal-section-label">Respond via Inbox</div><div class="modal-section-body" style="color:#6e7681;font-size:0.82em">Name your file <code style="background:#161b22;padding:2px 6px;border-radius:3px;user-select:all">task-${t.kanban_id}-yourfile.txt</code> and ${window._inboxHtml || 'drop it in <code style="background:#161b22;padding:2px 6px;border-radius:3px">saved_prompts/inbox/</code>'}</div></div>`;
  document.getElementById('modalBody').innerHTML = body;

  // Actions (emoji-only to keep compact)
  let actions = '';
  if (t.status === 'suggested') {
    actions = `<button class="k-btn approve" title="Approve &amp; start agent work" onclick="kanbanAction('approve-start','${t.kanban_id}');closeCardModal()">&#x2705;</button>`;
    actions += `<button class="k-btn" title="Assign to me" style="background:#1f3a5f;margin-left:6px" onclick="kanbanAction('assign-me','${t.kanban_id}');closeCardModal()">&#x1F464;</button>`;
    actions += `<button class="k-btn reject" title="Reject suggestion" style="margin-left:6px" onclick="rejectSuggestion('${t.kanban_id}');closeCardModal()">&#x1F5D1;</button>`;
  } else if (t.status === 'review') {
    actions = `<button class="k-btn approve" title="Accept result" onclick="kanbanAction('approve-result','${t.kanban_id}');closeCardModal()">&#x2705;</button>`;
    actions += `<button class="k-btn reject" title="Send back for re-work" style="margin-left:6px" onclick="kanbanReject('${t.kanban_id}');closeCardModal()">&#x274C;</button>`;
  } else if (t.status === 'done') {
    actions += `<button class="k-btn archive" title="Archive" onclick="archiveTask('${t.kanban_id}');closeCardModal()">&#x1F4E6;</button>`;
  }
  document.getElementById('modalActions').innerHTML = actions;

  document.getElementById('cardModal').classList.add('open');
}
function closeCardModal() {
  document.getElementById('cardModal').classList.remove('open');
}
function closeModal(e) {
  if (e.target.id === 'cardModal') closeCardModal();
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCardModal(); });

async function archiveTask(kanbanId) {
  try {
    const resp = await fetch('/api/kanban/archive', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({kanban_id: kanbanId}),
    });
    const data = await resp.json();
    showToast(data.message || 'Archived');
    poll();
  } catch(e) { showToast('Error: ' + e.message); }
}

// ── Kanban actions (POST) ─────────────────────────────
async function kanbanAction(action, kanbanId) {
  try {
    const resp = await fetch(`/api/kanban/${action}`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({kanban_id: kanbanId}),
    });
    const data = await resp.json();
    showToast(data.message || 'Done');
    poll();
  } catch(e) { showToast('Error: ' + e.message); }
}

function kanbanReject(kanbanId) {
  const notes = prompt('Feedback for the agent (optional):') || '';
  fetch('/api/kanban/reject', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({kanban_id: kanbanId, notes: notes}),
  }).then(() => { showToast('Sent back for re-work'); poll(); });
}

function rejectSuggestion(kanbanId) {
  // Skip confirm() — Simple Browser may not support native dialogs
  fetch('/api/kanban/reject-suggestion', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({kanban_id: kanbanId}),
  }).then(r => r.json()).then(d => { showToast(d.message || 'Rejected'); poll(); }).catch(e => showToast('Error: ' + e.message));
}

async function forceStart(taskId) {
  try {
    const resp = await fetch('/api/task/force-start', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({task_id: taskId}),
    });
    const data = await resp.json();
    showToast(data.message || 'Task restarted');
    poll();
  } catch(e) { showToast('Error: ' + e.message); }
}

function esc(s) {
  const el = document.createElement('span');
  el.textContent = s || '';
  return el.innerHTML;
}

// Load inbox info for tier-aware instructions
window._inboxHtml = '';
fetch('/api/inbox/info').then(r => r.json()).then(d => {
  if (d.instruction_html) window._inboxHtml = d.instruction_html;
}).catch(() => {});

// Poll immediately then every 5s
poll();
setInterval(poll, 5000);
// Load history on first expand
setInterval(() => { if (historyOpen) loadHistory(); }, 15000);

// ── Drawer ────────────────────────────────────────────
function toggleDrawer() {
  document.getElementById('helpDrawer').classList.toggle('open');
  document.getElementById('drawerOverlay').classList.toggle('open');
}
let activeTopic = '';
function filterDrawer() {
  const q = (document.getElementById('drawerSearch').value || '').toLowerCase();
  document.querySelectorAll('.cmd-item').forEach(el => {
    const text = (el.dataset.text || '').toLowerCase();
    el.classList.toggle('hidden', !((!activeTopic || text.includes(activeTopic)) && (!q || text.includes(q))));
  });
  document.querySelectorAll('.agent-card-mini').forEach(el => {
    const text = (el.dataset.text || '').toLowerCase();
    el.classList.toggle('hidden', !((!activeTopic || text.includes(activeTopic)) && (!q || text.includes(q))));
  });
}
function toggleTopic(topic, el) {
  if (activeTopic === topic) { activeTopic = ''; el.classList.remove('active'); }
  else { document.querySelectorAll('.topic-pill').forEach(p => p.classList.remove('active')); activeTopic = topic; el.classList.add('active'); }
  filterDrawer();
}
</script>
</body>
</html>"""


class NotificationFeed:
    """
    Serves a live activity feed + kanban board + command-help drawer.
    Designed to persist beyond CLI exit when opened in a VS Code Simple Browser tab.
    """

    def __init__(
        self,
        db_path: Path,
        port: int = 4545,
        state_dir: Path | None = None,
        user_tier: str = "open",
    ) -> None:
        self.db_path = db_path
        self.port = port
        self.user_tier = user_tier
        self._server: HTTPServer | None = None
        self._thread: threading.Thread | None = None
        # Callback for force-starting tasks (set by orchestrator)
        self.on_force_start: object | None = None
        self.on_kanban_approved: object | None = None
        # When True, callers (e.g. unified FastAPI server) handle callbacks; skip internal scheduling
        self._callbacks_handled_externally: bool = False
        # FileStore for persistent kanban data (git-tracked)
        from vastian.storage.file_store import FileStore

        sd = state_dir or Path("./state")
        self._kanban = FileStore(sd / "kanban.json", id_field="kanban_id")

    def _render_doc(self, path: str) -> tuple[str, int]:
        """Render a docs page from vastian-docs/. path: /docs or /docs/foo. Returns (html, status_code)."""
        raw = path.rstrip("/").replace("/docs", "", 1).lstrip("/") or "README"
        if ".." in raw or "/" in raw:
            return _docs_404_html(), 404
        # Try exact stem, then case-insensitive match
        candidates = [raw + ".md", raw.upper() + ".md", raw.capitalize() + ".md"]
        md_path = None
        for c in candidates:
            p = _DOCS_ROOT / c
            if p.exists():
                md_path = p
                break
        if not md_path:
            # Fallback: scan for stem match (e.g. architecture -> ARCHITECTURE.md)
            stem_lower = raw.lower().replace("-", "_")
            for f in _DOCS_ROOT.glob("*.md"):
                if f.stem.lower().replace("-", "_") == stem_lower:
                    md_path = f
                    break
        if not md_path:
            return _docs_404_html(), 404
        try:
            import markdown

            md_text = md_path.read_text(encoding="utf-8")
            body = markdown.markdown(md_text, extensions=["extra", "toc"])
            html = _docs_wrapper(raw, body)
            return html, 200
        except Exception as e:
            logger.exception("Docs render failed: %s", e)
            return _docs_404_html(), 500

    def start(self) -> str:
        """Start the feed server in a background thread. Returns the URL."""
        feed = self

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:  # noqa: N802
                try:
                    if self.path == "/api/notifications":
                        self._serve_json(feed._get_feed_data())
                    elif self.path == "/api/agents":
                        self._serve_json(AGENT_META)
                    elif self.path.startswith("/api/kanban"):
                        board_filter = None
                        if "?" in self.path:
                            qs = parse_qs(self.path.split("?", 1)[1])
                            board_filter = qs.get("board", [None])[0] or None
                        self._serve_json(feed._get_kanban_data(board=board_filter))
                    elif self.path == "/api/history":
                        self._serve_json(feed._get_history_data())
                    elif self.path == "/api/sync-suggestions":
                        self._serve_json(feed._get_sync_suggestions())
                    elif self.path == "/docs" or self.path.startswith("/docs/"):
                        self._serve_doc(feed, self.path)
                    else:
                        self._serve_html()
                except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
                    pass  # client disconnected mid-response — normal on Windows

            def do_POST(self) -> None:  # noqa: N802
                try:
                    content_len = int(self.headers.get("Content-Length", 0))
                    body = self.rfile.read(content_len).decode("utf-8") if content_len else "{}"
                    try:
                        data = json.loads(body)
                    except json.JSONDecodeError:
                        data = {}

                    result = {"ok": False, "message": "Unknown endpoint"}

                    if self.path == "/api/kanban/assign-me":
                        result = feed._assign_kanban_to_user(data.get("kanban_id", ""))
                    elif self.path == "/api/kanban/approve-start":
                        result = feed._approve_kanban_start(data.get("kanban_id", ""))
                    elif self.path == "/api/kanban/approve-result":
                        result = feed._approve_kanban_result(data.get("kanban_id", ""))
                    elif self.path == "/api/kanban/reject":
                        result = feed._reject_kanban(
                            data.get("kanban_id", ""), data.get("notes", "")
                        )
                    elif self.path == "/api/kanban/reject-suggestion":
                        result = feed._reject_kanban_suggestion(data.get("kanban_id", ""))
                    elif self.path == "/api/kanban/archive":
                        result = feed._archive_kanban_task(data.get("kanban_id", ""))
                    elif self.path == "/api/task/force-start":
                        result = feed._force_start_task(data.get("task_id", ""))

                    self._serve_json(result)
                except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
                    pass  # client disconnected mid-response

            def _serve_doc(self, feed: object, path: str) -> None:
                html, status = feed._render_doc(path)
                self.send_response(status)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Cache-Control", "no-store")
                self.end_headers()
                self.wfile.write(html.encode("utf-8"))

            def _serve_html(self) -> None:
                html = feed._render_html()
                self.send_response(200)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
                self.send_header("Pragma", "no-cache")
                self.send_header("Expires", "0")
                self.end_headers()
                self.wfile.write(html.encode("utf-8"))

            def _serve_json(self, data: object) -> None:
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps(data).encode("utf-8"))

            def log_message(self, format: str, *args: object) -> None:
                pass  # suppress HTTP logs

        # Subclass to set allow_reuse_address BEFORE bind — this lets us
        # reclaim the port from a daemon thread left over from a prior session.
        class ReusableHTTPServer(HTTPServer):
            allow_reuse_address = True

        try:
            self._server = ReusableHTTPServer(("127.0.0.1", self.port), Handler)
            self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
            self._thread.start()
            url = f"http://127.0.0.1:{self.port}"
            logger.info("Notification feed started at %s", url)
            return url
        except OSError as e:
            logger.warning("Could not start notification feed on port %d: %s", self.port, e)
            return ""

    def stop(self) -> None:
        if self._server:
            self._server.shutdown()
            self._server = None

    # ── Data fetchers ────────────────────────────────────

    def _get_feed_data(self) -> dict:
        """Read directly from SQLite for the latest state."""
        import sqlite3

        try:
            conn = sqlite3.connect(str(self.db_path))
            tasks = conn.execute(
                "SELECT * FROM background_tasks WHERE status IN ('queued','running') ORDER BY created_at DESC LIMIT 20"
            ).fetchall()
            notifications = conn.execute(
                "SELECT * FROM notifications ORDER BY created_at DESC LIMIT 30"
            ).fetchall()
            conn.close()
            return {
                "tasks": [
                    {
                        "task_id": r[0],
                        "task_type": r[1],
                        "agent_id": r[2],
                        "description": r[3],
                        "status": r[5],
                        "created_at": r[6],
                    }
                    for r in tasks
                ],
                "notifications": [
                    {
                        "id": r[0],
                        "task_id": r[1],
                        "agent_id": r[2],
                        "message": r[3],
                        "type": r[4],
                        "created_at": r[5],
                    }
                    for r in notifications
                ],
            }
        except Exception:
            return {"tasks": [], "notifications": []}

    def _get_kanban_data(self, board: str | None = None) -> dict:
        try:

            def _match(r: dict) -> bool:
                if r.get("status") == "archived":
                    return False
                return not (board and r.get("board", "agent") != board)

            tasks = self._kanban.query(_match)
            tasks.sort(key=lambda r: r.get("created_at", ""), reverse=True)
            return {"tasks": tasks}
        except Exception:
            return {"tasks": []}

    def _get_history_data(self) -> dict:
        import sqlite3

        try:
            conn = sqlite3.connect(str(self.db_path))
            tasks = conn.execute(
                "SELECT * FROM background_tasks WHERE status IN ('completed','failed') ORDER BY completed_at DESC LIMIT 100"
            ).fetchall()
            notifs = conn.execute(
                "SELECT * FROM notifications ORDER BY created_at DESC LIMIT 100"
            ).fetchall()
            conn.close()
            return {
                "tasks": [
                    {
                        "task_id": r[0],
                        "task_type": r[1],
                        "agent_id": r[2],
                        "description": r[3],
                        "status": r[5],
                        "created_at": r[6],
                        "completed_at": r[8],
                        "result": (r[9] or "")[:500],
                    }
                    for r in tasks
                ],
                "notifications": [
                    {
                        "id": r[0],
                        "task_id": r[1],
                        "agent_id": r[2],
                        "message": r[3],
                        "type": r[4],
                        "created_at": r[5],
                    }
                    for r in notifs
                ],
            }
        except Exception:
            return {"tasks": [], "notifications": []}

    def _get_sync_suggestions(self) -> dict:
        import sqlite3

        try:
            suggestions: list[dict] = []
            # Kanban tasks needing review (from FileStore)
            kanban_counts: dict[str, int] = {}
            for t in self._kanban.query(lambda r: r.get("status") in ("suggested", "review")):
                agent = t.get("suggested_by", "")
                kanban_counts[agent] = kanban_counts.get(agent, 0) + 1
            for agent_id, cnt in kanban_counts.items():
                suggestions.append(
                    {
                        "agent_id": agent_id,
                        "reason": f"{cnt} task(s) awaiting review",
                        "priority": "high",
                    }
                )
            # Running bg tasks (from SQLite)
            conn = sqlite3.connect(str(self.db_path))
            rows = conn.execute(
                "SELECT agent_id, COUNT(*) FROM background_tasks WHERE status='running' GROUP BY agent_id"
            ).fetchall()
            for r in rows:
                suggestions.append(
                    {
                        "agent_id": r[0],
                        "reason": f"{r[1]} background task(s) running",
                        "priority": "medium",
                    }
                )
            conn.close()
            return {"suggestions": suggestions}
        except Exception:
            return {"suggestions": []}

    # ── Action handlers ──────────────────────────────────

    def _assign_kanban_to_user(self, kanban_id: str) -> dict:
        """Assign a suggested task to the user — agents won't auto-start it."""
        try:
            from datetime import datetime

            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "suggested":
                return {"ok": False, "message": "Task not found or not in suggested status"}
            now = datetime.now(UTC).isoformat()
            self._kanban.update(
                kanban_id,
                {
                    "status": "approved",
                    "approved_at": now,
                    "assigned_to": "user",
                },
            )
            return {
                "ok": True,
                "message": "Task assigned to you — agents will not auto-start it. Drop a task-<id>-yourfile.txt in inbox/ when ready.",
            }
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _approve_kanban_start(self, kanban_id: str) -> dict:
        try:
            from datetime import datetime

            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "suggested":
                return {"ok": False, "message": "Task not found or already approved"}
            now = datetime.now(UTC).isoformat()
            self._kanban.update(kanban_id, {"status": "approved", "approved_at": now})
            # Trigger the callback if set (skip when unified server handles it)
            if self.on_kanban_approved and not self._callbacks_handled_externally:
                try:
                    import asyncio

                    loop = asyncio.get_event_loop()
                    if loop.is_running():
                        asyncio.ensure_future(self.on_kanban_approved(kanban_id))
                except Exception:
                    pass
            return {"ok": True, "message": "Task approved — agents will begin work"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _approve_kanban_result(self, kanban_id: str) -> dict:
        try:
            from datetime import datetime

            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "review":
                return {"ok": False, "message": "Task not found or not in review"}
            now = datetime.now(UTC).isoformat()
            self._kanban.update(kanban_id, {"status": "done", "reviewed_at": now})
            return {"ok": True, "message": "Result approved — task complete"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _reject_kanban(self, kanban_id: str, notes: str) -> dict:
        try:
            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "review":
                return {"ok": False, "message": "Task not found or not in review"}
            self._kanban.update(
                kanban_id, {"status": "approved", "user_notes": notes, "result": ""}
            )
            return {"ok": True, "message": "Sent back for re-work"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _reject_kanban_suggestion(self, kanban_id: str) -> dict:
        """Reject a suggested task — archives it immediately."""
        try:
            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "suggested":
                return {"ok": False, "message": "Task not found or not in suggested status"}
            from datetime import datetime

            now = datetime.now(UTC).isoformat()
            self._kanban.update(kanban_id, {"status": "rejected", "rejected_at": now})
            return {"ok": True, "message": "Suggestion rejected"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _archive_kanban_task(self, kanban_id: str) -> dict:
        try:
            task = self._kanban.get(kanban_id)
            if not task or task.get("status") != "done":
                return {"ok": False, "message": "Task not found or not in done status"}
            self._kanban.update(kanban_id, {"status": "archived"})
            return {"ok": True, "message": "Task archived"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _force_start_task(self, task_id: str) -> dict:
        import sqlite3

        try:
            conn = sqlite3.connect(str(self.db_path))
            cur = conn.execute(
                "UPDATE background_tasks SET status='queued' WHERE task_id=? AND status IN ('queued','running')",
                (task_id,),
            )
            conn.commit()
            conn.close()
            if cur.rowcount > 0:
                if self.on_force_start and not self._callbacks_handled_externally:
                    try:
                        import asyncio

                        loop = asyncio.get_event_loop()
                        if loop.is_running():
                            asyncio.ensure_future(self.on_force_start(task_id))
                    except Exception:
                        pass
                return {"ok": True, "message": "Task re-queued for execution"}
            return {"ok": False, "message": "Task not found"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _render_html(self, project_root=None) -> str:
        """Build the full HTML page with drawer content pre-rendered. Theme from config."""
        agent_opts = "".join(f'<option value="{a["id"]}">{a["name"]}</option>' for a in AGENT_META)
        all_topics: set[str] = set()
        for a in AGENT_META:
            all_topics.update(a.get("topics", []))
        for c in CLI_COMMANDS:
            all_topics.add(c.get("topic", ""))
        all_topics.discard("")
        pills_html = "".join(
            f'<span class="topic-pill" onclick="toggleTopic(\'{t}\', this)">{t}</span>'
            for t in sorted(all_topics)
        )
        cmd_html = "".join(
            f'<div class="cmd-item" data-text="{c["cmd"]} {c["desc"]} {c["topic"]}">'
            f"<code>{c['cmd']}</code><span>{c['desc']}</span></div>"
            for c in CLI_COMMANDS
        )
        agent_html = ""
        for a in AGENT_META:
            topics_spans = "".join(f"<span>{t}</span>" for t in a.get("topics", []))
            search_text = f"{a['id']} {a['name']} {a['title']} {' '.join(a.get('topics', []))}"
            agent_html += (
                f'<div class="agent-card-mini" data-text="{search_text}">'
                f'<span class="a-name">{a["name"]}</span>'
                f'<span class="a-title">{a["title"]}</span>'
                f'<div class="a-topics">{topics_spans}</div>'
                f"</div>"
            )
        # Board tabs — gated by access tier
        try:
            from vastian.access_tiers import visible_boards

            boards = visible_boards(self.user_tier)
        except Exception:
            boards = ["user", "comms", "company", "agent"]
        board_colors, board_labels_map = _load_boards_config()
        board_tabs_html = "".join(
            f'<button class="tab-btn" onclick="switchBoard(\'{b}\')" data-board="{b}" '
            f'style="font-size:0.82em;padding:8px 16px">{board_labels_map.get(b, b)}</button>'
            for b in boards
        )
        board_colors_js = json.dumps(board_colors, ensure_ascii=False)
        board_labels_js = json.dumps(board_labels_map, ensure_ascii=False)

        import time

        build_ts = str(int(time.time()))
        return (
            FEED_HTML.replace("__THEME_CSS__", _get_theme_css(project_root))
            .replace("__BOARD_COLORS__", board_colors_js)
            .replace("__BOARD_LABELS__", board_labels_js)
            .replace("__AGENT_OPTIONS__", agent_opts)
            .replace("__TOPIC_PILLS__", pills_html)
            .replace("__CMD_LIST__", cmd_html)
            .replace("__AGENT_LIST__", agent_html)
            .replace("__BOARD_TABS__", board_tabs_html)
            .replace("</title>", f"</title>\n<!-- build:{build_ts} -->")
        )
