"""Roster Page — browser-based session management UI for the Vastian Network.

Wave 6: Serves a session roster page at :4546 showing available session types,
active sessions, and session history. Tier-gated via access-tiers config.

Same architecture as feed.py and backup_panel.py:
  - Standalone HTTP server on a background thread
  - Single HTML page with JS polling
  - JSON API endpoints for session data
"""

from __future__ import annotations

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

logger = logging.getLogger(__name__)


# ── Icon mapping for session types ──────────────────────
ICON_MAP = {
    "rocket": "&#x1F680;",
    "sun": "&#x2600;",
    "target": "&#x1F3AF;",
    "crystal-ball": "&#x1F52E;",
    "heartbeat": "&#x1F49F;",
    "gear": "&#x2699;",
    "book": "&#x1F4DA;",
    "compass": "&#x1F9ED;",
    "palette": "&#x1F3A8;",
    "sparkles": "&#x2728;",
    "lotus": "&#x1FAB7;",
}


ROSTER_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<title>Vastian Sessions</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;
  }

  .top-bar {
    background: #161b22; border-bottom: 1px solid #30363d;
    padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;
    position: sticky; top: 0; z-index: 100;
  }
  .top-bar h1 { color: #58a6ff; font-size: 1.3em; }
  .top-bar .tier-badge {
    background: #21262d; color: #8b949e; padding: 4px 12px;
    border-radius: 12px; font-size: 0.8em;
  }

  .tab-bar {
    display: flex; background: #161b22; border-bottom: 1px solid #30363d;
    padding: 0 16px;
  }
  .tab-btn {
    padding: 10px 20px; color: #8b949e; cursor: pointer;
    border: none; background: none; font-size: 0.88em; 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 24px; }
  .tab-content.active { display: block; }

  h2 { color: #58a6ff; font-size: 1.1em; margin-bottom: 12px; }
  .subtitle { color: #8b949e; font-size: 0.85em; margin-bottom: 16px; }

  /* Session type cards (grid) */
  .type-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 12px; margin-bottom: 24px;
  }
  .type-card {
    background: #161b22; border: 1px solid #30363d; border-radius: 8px;
    padding: 16px; cursor: default; transition: border-color 0.15s;
    border-left: 3px solid var(--tc-color, #30363d);
  }
  .type-card:hover { border-color: var(--tc-color, #58a6ff); }
  /* Row 1: amber/gold */
  .type-card[data-row="1"] { --tc-color: #d29922; background: #1a1500; }
  /* Row 2: teal */
  .type-card[data-row="2"] { --tc-color: #39d2c0; background: #0a1a18; }
  /* Row 3: purple */
  .type-card[data-row="3"] { --tc-color: #a371f7; background: #150a1f; }
  /* Themed session: rainbow gradient border */
  .type-card[data-type="themed"] {
    border: none; padding: 15px;
    background: linear-gradient(135deg, #0d1117, #150a1f) padding-box,
                linear-gradient(135deg, #f85149, #d29922, #3fb950, #58a6ff, #a371f7, #f778ba) border-box;
    border: 2px solid transparent; border-radius: 10px;
  }
  .type-card[data-type="themed"]:hover {
    background: linear-gradient(135deg, #161b22, #1a0f2e) padding-box,
                linear-gradient(135deg, #f85149, #d29922, #3fb950, #58a6ff, #a371f7, #f778ba) border-box;
  }
  .type-card .tc-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
  .type-card .tc-icon { font-size: 1.4em; }
  .type-card .tc-label { font-weight: 600; color: #f0f6fc; font-size: 1em; }
  .type-card .tc-desc { color: #8b949e; font-size: 0.82em; margin-bottom: 10px; line-height: 1.4; }
  .type-card .tc-meta { color: #484f58; font-size: 0.75em; display: flex; gap: 12px; margin-bottom: 10px; }
  .type-card .tc-start {
    background: #238636; color: #fff; border: none; border-radius: 6px;
    padding: 6px 16px; font-size: 0.82em; cursor: pointer; font-family: inherit; font-weight: 600;
  }
  .type-card .tc-start:hover { background: #2ea043; }
  .type-card .tc-start:disabled { background: #30363d; color: #484f58; cursor: not-allowed; }

  /* Session list (active + history) */
  .session-list { display: flex; flex-direction: column; gap: 8px; }
  .session-card {
    background: #161b22; border: 1px solid #30363d; border-radius: 8px;
    padding: 14px 16px; display: flex; justify-content: space-between; align-items: center;
    cursor: pointer; transition: border-color 0.15s;
  }
  .session-card:hover { border-color: #58a6ff; }
  .session-card .sc-left { flex: 1; }
  .session-card .sc-name { font-weight: 600; color: #f0f6fc; font-size: 0.95em; }
  .session-card .sc-meta { color: #8b949e; font-size: 0.78em; margin-top: 3px; }
  .session-card .sc-theme { color: #6e7681; font-size: 0.75em; font-style: italic; margin-top: 2px; }
  .badge {
    font-size: 0.7em; padding: 2px 8px; border-radius: 10px; font-weight: 600;
    text-transform: uppercase;
  }
  .badge.open { background: #238636; color: #fff; }
  .badge.wrapping { background: #d29922; color: #0d1117; }
  .badge.completed { background: #30363d; color: #8b949e; }

  /* Session 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: 700px; 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 h3 { color: #f0f6fc; margin-bottom: 8px; font-size: 1.1em; padding-right: 40px; }
  .modal-card .round-block {
    background: #0d1117; border: 1px solid #21262d; border-radius: 6px;
    padding: 12px; margin: 10px 0; font-size: 0.85em; line-height: 1.5;
  }
  .modal-card .round-block .rb-sender { color: #58a6ff; font-weight: 600; margin-bottom: 4px; }
  .modal-card .round-block .rb-content { color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
  .modal-card .summary-block {
    background: #132a13; border: 1px solid #238636; border-radius: 6px;
    padding: 12px; margin-top: 14px; font-size: 0.85em; color: #3fb950;
  }

  .empty { color: #484f58; padding: 16px; text-align: center; }

  /* Toast */
  .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: 3000;
    pointer-events: none;
  }
  .toast.show { opacity: 1; }

  /* Admin tab */
  .admin-section { margin-bottom: 24px; }
  .admin-section h3 { color: #c9d1d9; font-size: 0.95em; margin-bottom: 8px; border-bottom: 1px solid #21262d; padding-bottom: 6px; }
  .admin-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 0; border-bottom: 1px solid #161b22; font-size: 0.85em;
  }
  .admin-row label { color: #c9d1d9; }
  .admin-row select {
    background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
    border-radius: 4px; padding: 3px 8px; font-size: 0.85em; font-family: inherit;
  }
  .admin-locked { color: #484f58; font-size: 0.82em; text-align: center; padding: 40px; }
</style>
</head>
<body>

<div class="top-bar">
  <h1>Vastian Sessions</h1>
  <span class="tier-badge">Tier: __USER_TIER__</span>
</div>

<div class="tab-bar">
  <button class="tab-btn active" onclick="switchTab('types')">Session Types</button>
  <button class="tab-btn" onclick="switchTab('active')">Active Sessions</button>
  <button class="tab-btn" onclick="switchTab('history')">History</button>
  __ADMIN_TAB__
</div>

<div class="tab-content active" id="tab-types">
  <h2>Available Session Types</h2>
  <p class="subtitle">Start a new session -- agents will be assembled based on the type</p>
  <div class="type-grid" id="typeGrid"><div class="empty">Loading...</div></div>
</div>

<div class="tab-content" id="tab-active">
  <h2>Active Sessions</h2>
  <p class="subtitle">Sessions currently in progress</p>
  <div class="session-list" id="activeSessions"><div class="empty">Loading...</div></div>
</div>

<div class="tab-content" id="tab-history">
  <h2>Session History</h2>
  <p class="subtitle">Completed typed sessions (presentations/requests are in the Activity Feed)</p>
  <div style="margin-bottom:10px">
    <select id="histTypeFilter" onchange="renderHistory()" style="background:#161b22;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:5px 10px;font-size:0.85em;font-family:inherit">
      <option value="">All types</option>
    </select>
  </div>
  <div class="session-list" id="historySessions"><div class="empty">Loading...</div></div>
</div>

<div class="tab-content" id="tab-admin">
  __ADMIN_CONTENT__
</div>

<!-- Session detail modal -->
<div class="modal-overlay" id="sessionModal" onclick="if(event.target.id==='sessionModal')closeModal()">
  <div class="modal-card">
    <button class="modal-close" onclick="closeModal()">&times;</button>
    <div id="modalBody"><div class="empty">Loading...</div></div>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
function esc(s) { const d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
function showToast(msg) {
  const t = document.getElementById('toast');
  t.textContent = msg; t.classList.add('show');
  setTimeout(() => t.classList.remove('show'), 2000);
}

// 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');
}

// Data
let sessionTypes = [];
let allSessions = [];

async function poll() {
  try {
    const [typesResp, sessionsResp] = await Promise.all([
      fetch('/api/session-types'),
      fetch('/api/sessions'),
    ]);
    sessionTypes = await typesResp.json();
    allSessions = await sessionsResp.json();
    renderTypes();
    renderActive();
    renderHistory();
  } catch(e) { /* server restarting */ }
}

let activeTypeFilter = '';

const TYPE_ROW = {
  onboarding:'1', daily_briefing:'1', cycle_planning:'1',
  wisdom_scenarios:'2', network_pulse:'2', backend_ops:'2',
  research_insights:'3', recalibration:'3', themed:'3',
};
const SPECIAL_TYPES = new Set(['recalibration', 'themed']);

function renderTypes() {
  const grid = document.getElementById('typeGrid');
  if (!sessionTypes.length) { grid.innerHTML = '<div class="empty">No session types configured</div>'; return; }
  grid.innerHTML = sessionTypes.map(t => {
    const icon = t.icon_html || '';
    const members = (t.default_members || []).join(', ');
    const locked = t.locked;
    const row = TYPE_ROW[t.type_id] || '1';
    const special = SPECIAL_TYPES.has(t.type_id) ? '<span style="background:#a371f733;color:#a371f7;font-size:0.68em;padding:2px 8px;border-radius:8px;margin-left:6px">special</span>' : '';
    const activeCount = allSessions.filter(s => s.session_type === t.type_id && (s.phase === 'open' || s.phase === 'wrapping')).length;
    const badge = activeCount > 0 ? `<span style="background:#238636;color:#fff;font-size:0.7em;padding:2px 7px;border-radius:10px;margin-left:6px">${activeCount} active</span>` : '';
    const lockLabel = locked ? `<div style="color:#f85149;font-size:0.75em;margin-top:6px">Requires ${esc(t.tier_required)} tier</div>` : '';
    const cardStyle = locked ? 'opacity:0.5' : '';

    let btns = '';
    if (locked) {
      btns = `<button class="tc-start" disabled style="opacity:0.4;cursor:not-allowed">Locked</button>`;
    } else if (activeCount > 0) {
      btns = `<button class="tc-start" onclick="viewTypeSessions('${t.type_id}')">View Sessions (${activeCount})</button>`
        + `<button class="tc-start" style="background:#30363d;color:#8b949e;margin-left:6px;font-size:0.75em" onclick="startSession('${t.type_id}')">+ New</button>`;
    } else {
      btns = `<button class="tc-start" onclick="startSession('${t.type_id}')">Start Session</button>`;
    }
    return `<div class="type-card" data-row="${row}" data-type="${t.type_id}" style="${cardStyle}">
      <div class="tc-header"><span class="tc-icon">${icon}</span><span class="tc-label">${esc(t.label)}${badge}${special}</span></div>
      <div class="tc-desc">${esc(t.description)}</div>
      <div class="tc-meta"><span>Lead: ${esc(t.lead)}</span><span>Members: ${esc(members)}</span><span>Rounds: ${t.max_rounds}</span></div>
      ${lockLabel}
      <div style="display:flex;align-items:center;margin-top:8px">${btns}</div>
    </div>`;
  }).join('');
}

function viewTypeSessions(typeId) {
  activeTypeFilter = typeId;
  switchTab('active');
  renderActive();
}

function renderActive() {
  const el = document.getElementById('activeSessions');
  let active = allSessions.filter(s => (s.phase === 'open' || s.phase === 'wrapping') && s.session_type);
  const filterLabel = activeTypeFilter ? ` (${activeTypeFilter})` : '';
  if (activeTypeFilter) {
    active = active.filter(s => s.session_type === activeTypeFilter);
  }
  const clearBtn = activeTypeFilter ? `<span style="color:#58a6ff;font-size:0.78em;cursor:pointer;margin-left:8px" onclick="activeTypeFilter='';renderActive()">clear filter</span>` : '';
  if (!active.length) { el.innerHTML = `<div class="empty">No active typed sessions${filterLabel}${clearBtn}</div>`; return; }
  el.innerHTML = (clearBtn ? `<div style="margin-bottom:8px;font-size:0.8em;color:#8b949e">Showing: ${activeTypeFilter}${clearBtn}</div>` : '')
    + active.map(s => {
    const name = s.display_name || s.topic || s.id;
    const type = s.session_type || '';
    const agents = (s.agents || []).join(', ');
    const theme = s.theme || '';
    const cardCount = (s.cards || []).length;
    const cardBadge = cardCount > 0 ? `<span style="background:#1f6feb;color:#fff;font-size:0.68em;padding:1px 6px;border-radius:8px;margin-left:6px">${cardCount} cards</span>` : '';
    return `<div class="session-card" onclick="openSession('${s.id}')">
      <div class="sc-left">
        <div class="sc-name">${esc(name)}${cardBadge}</div>
        <div class="sc-meta">${esc(type)} -- ${esc(agents)} -- ${s.rounds || 0} rounds</div>
        ${theme ? `<div class="sc-theme">${esc(theme)}</div>` : ''}
      </div>
      <span class="badge ${s.phase}">${s.phase}</span>
    </div>`;
  }).join('');
}

function renderHistory() {
  const el = document.getElementById('historySessions');
  // Only show typed sessions (not raw presentations/requests)
  let done = allSessions.filter(s => s.phase === 'completed' && s.session_type);
  if (!done.length) { el.innerHTML = '<div class="empty">No completed typed sessions yet</div>'; return; }

  // Type filter dropdown
  const types = [...new Set(done.map(s => s.session_type).filter(Boolean))].sort();
  const histFilter = document.getElementById('histTypeFilter');
  if (histFilter) {
    const cur = histFilter.value;
    if (cur) done = done.filter(s => s.session_type === cur);
  }

  el.innerHTML = done.slice(0, 50).map(s => {
    const name = s.display_name || s.topic || s.id;
    const type = s.session_type || '';
    const date = (s.completed_at || s.created_at || '').slice(0, 10);
    const cardCount = (s.cards || []).length;
    const cardBadge = cardCount > 0 ? `<span style="background:#30363d;color:#8b949e;font-size:0.68em;padding:1px 6px;border-radius:8px;margin-left:6px">${cardCount} cards</span>` : '';
    return `<div class="session-card" onclick="openSession('${s.id}')">
      <div class="sc-left">
        <div class="sc-name">${esc(name)}${cardBadge}</div>
        <div class="sc-meta">${esc(type)} -- ${date} -- ${s.rounds || 0} rounds</div>
      </div>
      <span class="badge completed">done</span>
    </div>`;
  }).join('');
}

async function startSession(typeId) {
  try {
    const resp = await fetch('/api/sessions/create', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({session_type: typeId}),
    });
    const data = await resp.json();
    if (data.ok) {
      showToast('Session started: ' + (data.display_name || typeId));
      switchTab('active');
      poll();
    } else {
      showToast('Error: ' + (data.message || 'Failed'));
    }
  } catch(e) { showToast('Error: ' + e.message); }
}

let currentSessionId = '';
let currentCards = [];
let currentCardIdx = 0;

async function openSession(sessionId) {
  try {
    const resp = await fetch('/api/session/' + sessionId);
    const s = await resp.json();
    if (!s || !s.id) { showToast('Session not found'); return; }

    currentSessionId = s.id;
    currentCards = s.cards || [];
    currentCardIdx = s.card_index || 0;

    const name = s.display_name || s.topic || s.id;
    const type = s.session_type || s.direction || '';
    const phase = s.phase || 'open';

    let body = `<h3>${esc(name)}</h3>`;
    body += `<div style="color:#8b949e;font-size:0.82em;margin-bottom:12px">${esc(type)} <span class="badge ${phase}">${phase}</span></div>`;

    if (currentCards.length > 0) {
      // Card carousel
      body += `<div id="cardCarousel"></div>`;
      body += `<div style="display:flex;justify-content:space-between;align-items:center;margin-top:14px">`;
      body += `  <button class="btn btn-reset" onclick="prevCard()" id="prevBtn" style="border:1px solid #30363d;background:#161b22;color:#8b949e;border-radius:6px;padding:6px 16px;cursor:pointer;font-family:inherit">Prev</button>`;
      body += `  <span id="cardIndicator" style="color:#8b949e;font-size:0.82em"></span>`;
      body += `  <button class="btn btn-save" onclick="nextCard()" id="nextBtn" style="background:#238636;color:#fff;border:none;border-radius:6px;padding:6px 16px;cursor:pointer;font-family:inherit">Next</button>`;
      body += `</div>`;
    } else if (s.rounds && s.rounds.length) {
      // Legacy: show rounds
      s.rounds.forEach(r => {
        body += `<div style="color:#484f58;font-size:0.75em;margin-top:12px">Round ${r.round_number} (${r.round_type || 'broadcast'})</div>`;
        if (r.initiator_message) {
          body += `<div class="round-block"><div class="rb-sender">${esc(r.initiator_message.sender_name || r.initiator_message.sender)}</div><div class="rb-content">${esc(r.initiator_message.content)}</div></div>`;
        }
        (r.responses || []).forEach(resp => {
          body += `<div class="round-block"><div class="rb-sender">${esc(resp.sender_name || resp.sender)}</div><div class="rb-content">${esc(resp.content)}</div></div>`;
        });
      });
      if (s.summary) body += `<div class="summary-block"><strong>Summary:</strong><br>${esc(s.summary)}</div>`;
    } else {
      body += '<div class="empty">No content yet.</div>';
    }

    document.getElementById('modalBody').innerHTML = body;
    document.getElementById('sessionModal').classList.add('open');
    if (currentCards.length > 0) renderCard();
  } catch(e) { showToast('Error loading session'); }
}

const CARD_TYPES = {
  content:        {icon:'&#x1F4DD;', color:'#58a6ff', label:'Briefing'},
  nugget:         {icon:'&#x1F4A1;', color:'#d29922', label:'Knowledge Nugget'},
  insight:        {icon:'&#x1F50D;', color:'#a371f7', label:'Insight'},
  quiz:           {icon:'&#x2753;',  color:'#f778ba', label:'Quick Check'},
  metrics_quiz:   {icon:'&#x1F4CA;', color:'#f85149', label:'Metrics Check-in'},
  config:         {icon:'&#x2699;',  color:'#d29922', label:'Settings'},
  persona_review: {icon:'&#x1F52E;', color:'#a371f7', label:'Persona Review'},
  action:         {icon:'&#x1F680;', color:'#3fb950', label:'Action Items'},
  metrics_review: {icon:'&#x1F4C8;', color:'#39d2c0', label:'Metrics Review'},
  reflection:     {icon:'&#x1F4AD;', color:'#79c0ff', label:'Reflection'},
};

function renderCard() {
  const carousel = document.getElementById('cardCarousel');
  if (!carousel || !currentCards.length) return;

  const card = currentCards[currentCardIdx];
  const ct = CARD_TYPES[card.type] || {icon:'&#x1F4DD;', color:'#8b949e', label:card.type};
  const agentBadge = card.agent_id ? `<span style="color:#58a6ff;font-size:0.75em;margin-left:8px">${esc(card.agent_id)}</span>` : '';

  let html = `<div style="background:#0d1117;border:1px solid #30363d;border-radius:10px;padding:20px;min-height:180px;border-left:3px solid ${ct.color}">`;
  html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">`;
  html += `<span style="font-size:1.3em">${ct.icon}</span>`;
  html += `<span style="color:${ct.color};font-weight:600;font-size:1.05em">${esc(card.title)}</span>`;
  html += agentBadge;
  html += `<span style="color:#484f58;font-size:0.68em;text-transform:uppercase;margin-left:auto;background:#161b22;padding:2px 8px;border-radius:8px">${ct.label}</span>`;
  html += `</div>`;

  // Type-specific body rendering
  if (card.type === 'nugget') {
    // Highlighted insight box
    html += `<div style="background:#d2992215;border:1px solid #d2992244;border-radius:8px;padding:14px;color:#ffa657;line-height:1.6;font-size:0.95em">${esc(card.content)}</div>`;
    if (card.metadata && card.metadata.source) {
      html += `<div style="color:#6e7681;font-size:0.72em;margin-top:6px">Source: ${esc(card.metadata.source)}</div>`;
    }

  } else if (card.type === 'insight') {
    // Callout with agent context
    html += `<div style="background:#a371f715;border:1px solid #a371f744;border-radius:8px;padding:14px;color:#d2a8ff;line-height:1.6;font-size:0.95em;font-style:italic">${esc(card.content)}</div>`;

  } else if (card.type === 'persona_review') {
    // Persona snapshot with trait highlights
    html += `<div style="color:#c9d1d9;line-height:1.6;white-space:pre-wrap;margin-bottom:10px">${esc(card.content)}</div>`;
    if (card.metadata && card.metadata.traits) {
      html += `<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">`;
      Object.entries(card.metadata.traits).forEach(([trait, val]) => {
        const pct = Math.round((val || 0) * 100);
        const barColor = pct > 70 ? '#3fb950' : pct > 40 ? '#d29922' : '#f85149';
        html += `<div style="background:#161b22;border:1px solid #21262d;border-radius:6px;padding:4px 10px;font-size:0.75em;min-width:120px">`;
        html += `<div style="color:#8b949e">${esc(trait)}</div>`;
        html += `<div style="background:#21262d;height:4px;border-radius:2px;margin-top:3px"><div style="background:${barColor};height:100%;width:${pct}%;border-radius:2px"></div></div>`;
        html += `</div>`;
      });
      html += `</div>`;
    }

  } else if (card.type === 'metrics_review') {
    // Metrics with trend arrows
    html += `<div style="color:#c9d1d9;line-height:1.6;white-space:pre-wrap;margin-bottom:10px">${esc(card.content)}</div>`;
    if (card.metadata && card.metadata.metrics) {
      html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-top:8px">`;
      Object.entries(card.metadata.metrics).forEach(([name, data]) => {
        const val = typeof data === 'object' ? data.value : data;
        const trend = typeof data === 'object' ? (data.trend || 0) : 0;
        const arrow = trend > 0 ? '&#x2191;' : trend < 0 ? '&#x2193;' : '&#x2192;';
        const tColor = trend > 0 ? '#3fb950' : trend < 0 ? '#f85149' : '#8b949e';
        html += `<div style="background:#161b22;border:1px solid #21262d;border-radius:6px;padding:10px;text-align:center">`;
        html += `<div style="color:#8b949e;font-size:0.72em">${esc(name)}</div>`;
        html += `<div style="color:#f0f6fc;font-size:1.3em;font-weight:700">${val}%</div>`;
        html += `<div style="color:${tColor};font-size:0.8em">${arrow} ${trend > 0 ? '+' : ''}${trend}%</div>`;
        html += `</div>`;
      });
      html += `</div>`;
    }

  } else if (card.type === 'metrics_quiz') {
    // Multi-question metrics check-in
    html += `<div style="color:#c9d1d9;line-height:1.6;margin-bottom:12px">${esc(card.content)}</div>`;
    if (card.options) {
      const answers = card.answer || {};
      html += `<div style="margin-top:8px">`;
      card.options.forEach((q, qi) => {
        const qText = typeof q === 'object' ? q.question : q;
        const qKey = typeof q === 'object' ? (q.key || qi) : qi;
        const answered = answers[qKey] != null;
        html += `<div style="margin:8px 0;padding:10px;background:#161b22;border:1px solid #21262d;border-radius:6px">`;
        html += `<div style="color:#c9d1d9;font-size:0.88em;margin-bottom:6px">${esc(qText)}</div>`;
        html += `<div style="display:flex;gap:4px">`;
        for (let v = 1; v <= 10; v++) {
          const sel = answers[qKey] === v;
          const st = sel ? 'background:#1f6feb;color:#fff' : answered ? 'opacity:0.4' : 'cursor:pointer;background:#0d1117';
          html += `<div onclick="${answered ? '' : `selectMetric(${currentCardIdx},'${qKey}',${v})`}" style="border:1px solid #30363d;border-radius:4px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:0.75em;${st}">${v}</div>`;
        }
        html += `</div></div>`;
      });
      html += `</div>`;
    }

  } else if (card.type === 'reflection') {
    // Journaling prompt with text area
    html += `<div style="color:#79c0ff;line-height:1.6;margin-bottom:12px;font-style:italic;font-size:1.05em">${esc(card.content)}</div>`;
    const savedText = card.answer || '';
    html += `<textarea id="reflectionInput" style="width:100%;min-height:100px;background:#161b22;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:10px;font-family:inherit;font-size:0.88em;resize:vertical" placeholder="Write your reflection...">${esc(savedText)}</textarea>`;
    html += `<button onclick="saveReflection(${currentCardIdx})" style="margin-top:8px;background:#1f6feb;color:#fff;border:none;border-radius:6px;padding:6px 16px;font-size:0.82em;cursor:pointer;font-family:inherit">Save Reflection</button>`;

  } else if (card.type === 'action') {
    // Checklist
    html += `<div style="color:#c9d1d9;line-height:1.8;white-space:pre-wrap">${esc(card.content)}</div>`;
    if (card.options) {
      const checked = card.answer || [];
      html += `<div style="margin-top:10px">`;
      card.options.forEach((item, i) => {
        const done = checked.includes(i);
        html += `<div onclick="toggleAction(${currentCardIdx},${i})" style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;border-bottom:1px solid #21262d">`;
        html += `<span style="color:${done ? '#3fb950' : '#30363d'};font-size:1.1em">${done ? '&#x2705;' : '&#x2B1C;'}</span>`;
        html += `<span style="color:${done ? '#6e7681' : '#c9d1d9'};${done ? 'text-decoration:line-through' : ''}">${esc(item)}</span>`;
        html += `</div>`;
      });
      html += `</div>`;
    }

  } else if (card.type === 'config' && card.config_file) {
    // L7: Config suggestion card — Accept / Reject
    const answered = card.answer === 'accepted' || card.answer === 'rejected';
    const listOp = card.list_operation;
    const listItem = card.list_item;
    html += `<div style="color:#c9d1d9;line-height:1.6;white-space:pre-wrap">${esc(card.content || card.reason || '')}</div>`;
    html += `<div style="margin-top:12px;font-size:0.88em;color:#8b949e">${esc(card.config_file)} &rarr; ${esc(card.field)}</div>`;
    html += `<div style="margin-top:6px;display:flex;gap:8px"><span style="color:#6e7681">Current:</span><code style="background:#161b22;padding:2px 6px;border-radius:4px">${esc(String(card.current_value || ''))}</code></div>`;
    if (listOp && listItem) {
      html += `<div style="margin-top:4px;display:flex;gap:8px"><span style="color:#8b949e">List ${listOp}:</span><code style="background:#161b22;padding:2px 6px;border-radius:4px;color:#3fb950">${esc(String(listItem))}</code></div>`;
    } else {
      html += `<div style="margin-top:4px;display:flex;gap:8px"><span style="color:#8b949e">Proposed:</span><code style="background:#161b22;padding:2px 6px;border-radius:4px;color:#3fb950">${esc(String(card.proposed_value || ''))}</code></div>`;
    }
    if (!answered) {
      html += `<div style="margin-top:14px;display:flex;gap:10px">`;
      html += `<button onclick="configApprove(${currentCardIdx}, true)" style="background:#238636;color:#fff;border:none;border-radius:6px;padding:8px 16px;font-size:0.88em;cursor:pointer;font-family:inherit">Accept</button>`;
      html += `<button onclick="configApprove(${currentCardIdx}, false)" style="background:#30363d;color:#c9d1d9;border:1px solid #484f58;border-radius:6px;padding:8px 16px;font-size:0.88em;cursor:pointer;font-family:inherit">Reject</button>`;
      html += `</div>`;
    } else {
      html += `<div style="margin-top:10px;color:${card.answer === 'accepted' ? '#3fb950' : '#8b949e'}">${card.answer === 'accepted' ? '&#x2705; Accepted' : '&#x2716; Rejected'}</div>`;
    }

  } else {
    // Default: content, config, quiz (existing behavior)
    html += `<div style="color:#c9d1d9;line-height:1.6;white-space:pre-wrap">${esc(card.content)}</div>`;
    if ((card.type === 'config' || card.type === 'quiz') && card.options) {
      const answered = card.answer != null;
      html += `<div style="margin-top:14px">`;
      card.options.forEach((opt, i) => {
        const selected = card.answer === opt;
        const style = selected
          ? 'background:#1f6feb;color:#fff;border-color:#1f6feb'
          : answered ? 'opacity:0.5' : 'cursor:pointer';
        html += `<div onclick="${answered ? '' : `selectOption(${currentCardIdx},${i})`}" style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:8px 12px;margin:4px 0;font-size:0.88em;transition:all 0.15s;${style}">${esc(opt)}</div>`;
      });
      html += `</div>`;
    }
  }

  html += `</div>`;
  carousel.innerHTML = html;

  // Update nav
  const total = currentCards.length;
  document.getElementById('cardIndicator').textContent = `${currentCardIdx + 1} / ${total}`;
  document.getElementById('prevBtn').disabled = currentCardIdx === 0;
  document.getElementById('prevBtn').style.opacity = currentCardIdx === 0 ? '0.3' : '1';

  const isLast = currentCardIdx === total - 1;
  const nextBtn = document.getElementById('nextBtn');
  if (isLast) {
    nextBtn.textContent = 'Complete Session';
    nextBtn.style.background = '#238636';
    nextBtn.onclick = () => completeSession();
  } else {
    nextBtn.textContent = 'Next';
    nextBtn.style.background = '#1f6feb';
    nextBtn.onclick = () => nextCard();
  }
}

// Metrics quiz handler
async function selectMetric(cardIdx, key, value) {
  const card = currentCards[cardIdx];
  if (!card) return;
  if (!card.answer) card.answer = {};
  card.answer[key] = value;
  renderCard();
  fetch('/api/sessions/answer', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({session_id: currentSessionId, card_index: cardIdx, answer: card.answer}),
  });
}

// Reflection save handler
async function saveReflection(cardIdx) {
  const text = document.getElementById('reflectionInput').value;
  if (!text.trim()) return;
  currentCards[cardIdx].answer = text;
  renderCard();
  fetch('/api/sessions/answer', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({session_id: currentSessionId, card_index: cardIdx, answer: text}),
  });
}

// Action checklist toggle
async function toggleAction(cardIdx, itemIdx) {
  const card = currentCards[cardIdx];
  if (!card) return;
  if (!card.answer) card.answer = [];
  if (card.answer.includes(itemIdx)) {
    card.answer = card.answer.filter(i => i !== itemIdx);
  } else {
    card.answer.push(itemIdx);
  }
  renderCard();
  fetch('/api/sessions/answer', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({session_id: currentSessionId, card_index: cardIdx, answer: card.answer}),
  });
}

function prevCard() { if (currentCardIdx > 0) { currentCardIdx--; renderCard(); } }
function nextCard() { if (currentCardIdx < currentCards.length - 1) { currentCardIdx++; renderCard(); } }

async function selectOption(cardIdx, optIdx) {
  const card = currentCards[cardIdx];
  if (!card || !card.options) return;
  card.answer = card.options[optIdx];
  renderCard();
  // Save to backend
  fetch('/api/sessions/answer', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({session_id: currentSessionId, card_index: cardIdx, answer: card.answer}),
  });
}

async function configApprove(cardIdx, accepted) {
  const card = currentCards[cardIdx];
  if (!card || card.type !== 'config') return;
  const resp = await fetch('/api/sessions/config-approve', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({session_id: currentSessionId, card_index: cardIdx, accepted: accepted}),
  });
  const data = await resp.json();
  if (data.ok) {
    card.answer = accepted ? 'accepted' : 'rejected';
    renderCard();
  }
}

async function completeSession() {
  try {
    const resp = await fetch('/api/sessions/complete', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({session_id: currentSessionId}),
    });
    const data = await resp.json();
    if (data.ok) {
      showToast('Session completed!');
      closeModal();
      poll();
    } else {
      showToast('Error: ' + (data.message || 'Failed'));
    }
  } catch(e) { showToast('Error: ' + e.message); }
}

function closeModal() { document.getElementById('sessionModal').classList.remove('open'); }
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });

// Poll
poll();
setInterval(poll, 8000);
</script>
</body>
</html>"""


class RosterPage:
    """
    Serves the session roster browser page at a configurable port.
    Same pattern as NotificationFeed and BackupPanel.
    """

    def __init__(
        self,
        port: int = 4546,
        state_dir: Path | None = None,
        user_tier: str = "open",
    ) -> None:
        self.port = port
        self.user_tier = user_tier
        self._server: HTTPServer | None = None
        self._thread: threading.Thread | None = None

        # Session engine reference (set after construction)
        self.session_engine: object | None = None
        # Orchestrator reference (for starting sessions)
        self.orchestrator: object | None = None

        sd = state_dir or Path("./state")
        self._state_dir = sd
        from vastian.storage.file_store import FileStore

        self._sessions_store = FileStore(sd / "sessions.json", id_field="id")

    def _append_config_proposal_audit(
        self,
        config_file: str,
        field: str,
        current_value: str,
        proposed_value: str,
        reason: str,
        outcome: str,
        session_id: str = "",
        list_operation: str | None = None,
        list_item: str | None = None,
        message: str | None = None,
    ) -> None:
        """Append an entry to state/config-proposals-audit.json."""
        from datetime import datetime

        audit_path = self._state_dir / "config-proposals-audit.json"
        entry = {
            "timestamp": datetime.now(UTC).isoformat(),
            "config_file": config_file,
            "field": field,
            "current_value": current_value,
            "proposed_value": proposed_value,
            "reason": reason,
            "outcome": outcome,
            "session_id": session_id,
            "list_operation": list_operation,
            "list_item": list_item,
        }
        if message:
            entry["message"] = message
        try:
            data = json.loads(audit_path.read_text(encoding="utf-8")) if audit_path.exists() else []
            if not isinstance(data, list):
                data = []
            data.append(entry)
            audit_path.parent.mkdir(parents=True, exist_ok=True)
            audit_path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        except Exception as e:
            logger.warning("Config proposals audit write failed: %s", e)

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

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:  # noqa: N802
                try:
                    if self.path == "/api/session-types":
                        self._serve_json(roster._get_session_types())
                    elif self.path == "/api/sessions":
                        self._serve_json(roster._get_sessions())
                    elif self.path.startswith("/api/session/"):
                        session_id = self.path.split("/api/session/")[1].split("?")[0]
                        self._serve_json(roster._get_session_detail(session_id))
                    else:
                        self._serve_html()
                except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
                    pass

            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/sessions/create":
                        result = roster._create_session(data)
                    elif self.path == "/api/sessions/complete":
                        result = roster._complete_session(data)
                    elif self.path == "/api/sessions/answer":
                        result = roster._save_card_answer(data)
                    elif self.path == "/api/sessions/config-approve":
                        result = roster._config_approve(data)

                    self._serve_json(result)
                except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
                    pass

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

            def _serve_html(self) -> None:
                html = roster._render_html()
                body = html.encode("utf-8")
                self.send_response(200)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Content-Length", str(len(body)))
                self.send_header("Cache-Control", "no-store")
                self.end_headers()
                self.wfile.write(body)

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

        class ReusableHTTPServer(HTTPServer):
            allow_reuse_address = True
            allow_reuse_port = True

        try:
            self._server = ReusableHTTPServer(("127.0.0.1", self.port), Handler)
        except OSError:
            logger.warning("Roster port %d in use, trying %d", self.port, self.port + 10)
            self.port += 10
            self._server = ReusableHTTPServer(("127.0.0.1", self.port), Handler)

        self._thread = threading.Thread(
            target=self._server.serve_forever,
            daemon=True,
            name="roster-page",
        )
        self._thread.start()
        url = f"http://127.0.0.1:{self.port}"
        logger.info("Roster page started at %s", url)
        return url

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

    # ── API handlers ──────────────────────────────────

    def _get_session_types(self) -> list[dict]:
        """Return session types available at the user's tier."""
        try:
            if self.session_engine and hasattr(self.session_engine, "get_session_types"):
                types = self.session_engine.get_session_types(self.user_tier)
                # Add icon HTML
                for t in types:
                    t["icon_html"] = ICON_MAP.get(t.get("icon", ""), "")
                return types
        except Exception as e:
            logger.debug("Error loading session types: %s", e)
        return []

    _VISIBLE_PHASES = {"ready", "active", "completed"}

    def _get_sessions(self) -> list[dict]:
        """Return session summaries visible to the user (ready/active/completed only)."""
        try:
            sessions = self._sessions_store.load_all()
            # Build summary dicts — filter out draft/planning/curating phases
            result = []
            for s in sessions:
                phase = s.get("phase", "")
                if phase and phase not in self._VISIBLE_PHASES:
                    continue
                result.append(
                    {
                        "id": s.get("id", ""),
                        "direction": s.get("direction", ""),
                        "topic": s.get("topic", ""),
                        "phase": phase,
                        "agents": s.get("agent_ids", []),
                        "rounds": s.get("current_round", 0),
                        "session_type": s.get("session_type"),
                        "display_name": s.get("display_name"),
                        "theme": s.get("theme"),
                        "lead_agent": s.get("lead_agent"),
                        "created_at": s.get("created_at", ""),
                        "completed_at": s.get("completed_at"),
                    }
                )
            result.sort(key=lambda s: s.get("created_at", ""), reverse=True)
            return result
        except Exception:
            return []

    def _get_session_detail(self, session_id: str) -> dict:
        """Return full session data for the detail modal."""
        try:
            s = self._sessions_store.get(session_id)
            return s if s else {"error": "Session not found"}
        except Exception:
            return {"error": "Session not found"}

    # ── Card templates per session type ─────────────────

    CARD_TEMPLATES: dict[str, list[dict]] = {
        "onboarding": [
            {
                "type": "content",
                "title": "Welcome",
                "content": "Welcome to the Vastian Network! This onboarding session will introduce you to the key agents and how they work together.",
            },
            {
                "type": "content",
                "title": "Your Team",
                "content": "You have 20 agents organized into divisions: Core Staff, Strategy, Nexus (Work Intelligence), Scenario Lab, and the Frontend Roster.",
            },
            {
                "type": "config",
                "title": "Focus Areas",
                "content": "Which areas would you like to focus on first?",
                "options": [
                    "Career & Work",
                    "Personal Growth",
                    "Health & Wellness",
                    "Relationships",
                    "All of the above",
                ],
            },
            {
                "type": "content",
                "title": "Getting Started",
                "content": "Start by chatting in the CLI with /agent <id> to talk to specific agents, or let Charles triage your requests automatically.",
            },
            {
                "type": "action",
                "title": "Next Steps",
                "content": "1. Drop an intro.txt in saved_prompts/inbox/ (or upload via cloud inbox) to onboard your agents\n2. Run /suggest to populate the Kanban boards\n3. Check /check-in for pending agent outreach",
            },
        ],
        "daily_briefing": [
            {
                "type": "content",
                "title": "Today's Overview",
                "content": "Charles has prepared your daily briefing. Here's what needs attention today.",
                "agent_id": "charles",
            },
            {
                "type": "nugget",
                "title": "Knowledge Highlight",
                "content": "Pattern detected: your best focus hours are between 9-11am. Consider scheduling deep work during this window.",
                "agent_id": "elias",
                "metadata": {"source": "DIKW promotion from last 7 days"},
            },
            {
                "type": "metrics_review",
                "title": "Weekly Pulse",
                "content": "Your key metrics this week:",
                "agent_id": "felix",
                "metadata": {
                    "metrics": {
                        "Alignment": {"value": 78, "trend": 3},
                        "Momentum": {"value": 65, "trend": -5},
                        "Focus": {"value": 82, "trend": 7},
                    }
                },
            },
            {
                "type": "config",
                "title": "Quick Settings",
                "content": "Based on this week's activity, should we adjust?",
                "options": [
                    "Increase check-in frequency",
                    "Keep current settings",
                    "Reduce notifications",
                ],
            },
            {
                "type": "action",
                "title": "Today's Actions",
                "content": "Priority items for today:",
                "options": [
                    "Review 3 Kanban suggestions",
                    "Address agent outreach items",
                    "Update stale documents",
                    "Run /check-in for overdue items",
                ],
            },
        ],
        "cycle_planning": [
            {
                "type": "content",
                "title": "Sprint Planning",
                "content": "Liam will coordinate this cycle planning session. Let's review open work and set priorities.",
            },
            {
                "type": "content",
                "title": "Open Tickets",
                "content": "Review tickets across all boards. Arjuna will help prioritize and assign to the sprint.",
            },
            {
                "type": "config",
                "title": "Sprint Goal",
                "content": "What is the main goal for this sprint?",
                "options": [
                    "Ship a feature",
                    "Clear tech debt",
                    "Documentation",
                    "Personal goals",
                    "Mixed",
                ],
            },
            {
                "type": "content",
                "title": "Capacity",
                "content": "Review agent capacity and assign story points. Noah will generate a creative theme for the sprint.",
            },
            {
                "type": "action",
                "title": "Sprint Kickoff",
                "content": "1. Finalize sprint tickets\n2. Assign story points\n3. Set sprint deadline\n4. Announce theme to the team",
            },
        ],
        "wisdom_scenarios": [
            {
                "type": "content",
                "title": "Scenario Lab",
                "content": "Concord and Kiran will guide this exploration of what-if scenarios and strategic foresight.",
            },
            {
                "type": "config",
                "title": "Scenario Focus",
                "content": "What domain should we explore?",
                "options": ["Professional", "Personal", "Side Venture", "Relationships", "Health"],
            },
            {
                "type": "content",
                "title": "Variables",
                "content": "Kiran will assess relevant simulation variables and Jake will validate the metrics.",
            },
            {
                "type": "quiz",
                "title": "Quick Assessment",
                "content": "On a scale of 1-5, how confident are you in your current direction?",
                "options": [
                    "1 - Very uncertain",
                    "2 - Somewhat uncertain",
                    "3 - Neutral",
                    "4 - Fairly confident",
                    "5 - Very confident",
                ],
            },
            {
                "type": "action",
                "title": "Insights",
                "content": "Review the scenario results and decide on next steps with Victor's vision alignment.",
            },
        ],
        "network_pulse": [
            {
                "type": "content",
                "title": "Network Health",
                "content": "Felix will lead this health check of the Vastian Network.",
            },
            {
                "type": "content",
                "title": "Agent Status",
                "content": "Overview of all 20 agents: active tasks, recent completions, and any issues.",
            },
            {
                "type": "content",
                "title": "System Metrics",
                "content": "Background loop health, task queue status, and notification feed activity.",
            },
            {
                "type": "action",
                "title": "Recommendations",
                "content": "Felix's suggestions for improving network health and agent coordination.",
            },
        ],
        "backend_ops": [
            {
                "type": "content",
                "title": "Ops Review",
                "content": "Orion leads this review of backend operations, infrastructure, and quality.",
            },
            {
                "type": "content",
                "title": "Relay Reports",
                "content": "Maria will present triaged relay outbox reports from Theo, Orion, and other backend agents.",
            },
            {
                "type": "content",
                "title": "Quality Audit",
                "content": "Theo's prompt quality findings and drift detection results.",
            },
            {
                "type": "action",
                "title": "Ops Actions",
                "content": "1. Address critical relay findings\n2. Review prompt quality issues\n3. Update systems tracker",
            },
        ],
        "research_insights": [
            {
                "type": "content",
                "title": "Knowledge Synthesis",
                "content": "Elias leads this session on recent learnings and knowledge promotion.",
            },
            {
                "type": "content",
                "title": "Recent Insights",
                "content": "Patterns and insights extracted from recent interactions, meetings, and documents.",
            },
            {
                "type": "content",
                "title": "DIKW Status",
                "content": "Knowledge items ready for promotion through the Data-Information-Knowledge-Wisdom pyramid.",
            },
            {
                "type": "action",
                "title": "Research Actions",
                "content": "1. Promote key insights\n2. Flag knowledge gaps\n3. Update knowledge bank",
            },
        ],
        "recalibration": [
            {
                "type": "content",
                "title": "Recalibration",
                "content": "Your digital twin leads this periodic resync of goals, habits, and priorities.",
                "agent_id": "rowan",
            },
            {
                "type": "metrics_quiz",
                "title": "Life Check-in",
                "content": "Rate how you feel about each area right now:",
                "agent_id": "kiran",
                "options": [
                    {
                        "question": "Purpose clarity - do you know what you're working toward?",
                        "key": "purpose_clarity",
                    },
                    {
                        "question": "Focus intensity - can you concentrate on what matters?",
                        "key": "focus_intensity",
                    },
                    {
                        "question": "Emotional resilience - how well are you handling stress?",
                        "key": "emotional_resilience",
                    },
                    {
                        "question": "Creative output - are you producing meaningful work?",
                        "key": "creative_output",
                    },
                    {
                        "question": "Energy alignment - does your daily energy match your goals?",
                        "key": "energy_alignment",
                    },
                ],
            },
            {
                "type": "persona_review",
                "title": "Persona Snapshot",
                "content": "Your dominant persona patterns this period:",
                "agent_id": "adrian",
                "metadata": {
                    "traits": {
                        "Discipline": 0.65,
                        "Creativity": 0.60,
                        "Wisdom": 0.70,
                        "Visionary Insight": 0.75,
                        "Manifestation": 0.72,
                        "Spiritual Vision": 0.80,
                    }
                },
            },
            {
                "type": "insight",
                "title": "Sage's Observation",
                "content": "Your stress triggers correlate with sprint planning mornings. Consider a grounding routine before Monday standups.",
                "agent_id": "sage",
            },
            {
                "type": "config",
                "title": "Priority Update",
                "content": "Which areas need the most attention going forward?",
                "options": [
                    "Career growth",
                    "Health & wellness",
                    "Relationships",
                    "Personal projects",
                    "Financial goals",
                    "Spiritual alignment",
                ],
            },
            {
                "type": "reflection",
                "title": "Open Reflection",
                "content": "What has changed about how you see yourself since the last recalibration? What feels different?",
                "agent_id": "rowan",
            },
            {
                "type": "action",
                "title": "Recalibration Actions",
                "content": "Based on your check-in:",
                "options": [
                    "Update vision documents",
                    "Adjust sprint priorities",
                    "Schedule follow-up check-ins",
                    "Review persona evolution",
                    "Book therapy session",
                ],
            },
        ],
        "themed": [
            {
                "type": "content",
                "title": "Creative Session",
                "content": "Noah has prepared a themed creative session. Let your imagination run free!",
            },
            {
                "type": "config",
                "title": "Theme Choice",
                "content": "Pick a creative direction:",
                "options": [
                    "Future visioning",
                    "Storytelling",
                    "Problem reframing",
                    "Wild brainstorm",
                    "Surprise me",
                ],
            },
            {
                "type": "content",
                "title": "Creative Prompt",
                "content": "Noah will generate a creative prompt based on your choice. Adrian and Victor bring their perspectives.",
            },
            {
                "type": "quiz",
                "title": "Reflection",
                "content": "What resonated most from this creative exploration?",
                "options": [
                    "A new idea emerged",
                    "I saw a problem differently",
                    "I feel more energized",
                    "I want to explore more",
                ],
            },
            {
                "type": "action",
                "title": "Creative Output",
                "content": "Capture any ideas, insights, or next steps that emerged from this session.",
            },
        ],
    }

    def _create_session(self, data: dict) -> dict:
        """Create a new typed session with pre-prepared cards."""
        session_type = data.get("session_type", "")
        topic = data.get("topic", "")

        if not session_type:
            return {"ok": False, "message": "session_type is required"}

        if not self.session_engine:
            return {"ok": False, "message": "Session engine not connected"}

        try:
            if self.session_engine and hasattr(self.session_engine, "get_session_type_config"):
                tc = self.session_engine.get_session_type_config(session_type)
                if not tc:
                    return {"ok": False, "message": f"Unknown session type: {session_type}"}

                # Generate cards from template
                cards = []
                for card in self.CARD_TEMPLATES.get(session_type, []):
                    cards.append({**card, "answer": None})

                # L7: Inject pending config cards for backend_ops / recalibration
                if session_type in ("backend_ops", "recalibration"):
                    pending_path = self._state_dir / "pending-config-cards.json"
                    if pending_path.exists():
                        try:
                            from vastian.storage.file_store import FileStore

                            config_store = FileStore(pending_path, id_field="suggestion_id")
                            pending = config_store.load_all()
                            for p in pending:
                                card = {
                                    "type": "config",
                                    "title": "Config suggestion",
                                    "content": p.get("reason", ""),
                                    "suggestion_id": p.get("suggestion_id"),
                                    "config_file": p.get("config_file", ""),
                                    "field": p.get("field", ""),
                                    "current_value": p.get("current_value", ""),
                                    "proposed_value": p.get("proposed_value", ""),
                                    "reason": p.get("reason", ""),
                                    "answer": None,
                                }
                                if p.get("list_operation") and p.get("list_item"):
                                    card["list_operation"] = p["list_operation"]
                                    card["list_item"] = p["list_item"]
                                cards.append(card)
                                config_store.delete(p.get("suggestion_id", ""))
                        except Exception as e:
                            logger.debug("Inject pending config cards: %s", e)

                import uuid
                from datetime import datetime

                session_id = uuid.uuid4().hex[:12]
                record = {
                    "id": session_id,
                    "direction": "presentation",
                    "topic": topic or tc["label"],
                    "session_type": session_type,
                    "display_name": tc["label"],
                    "theme": None,
                    "lead_agent": tc["lead"],
                    "presenter_name": tc["lead"],
                    "agent_ids": tc["default_members"],
                    "phase": "open",
                    "rounds": [],
                    "current_round": 0,
                    "cards": cards,
                    "card_index": 0,
                    "summary": "",
                    "user_id": getattr(self.orchestrator, "current_user_id", "default")
                    if self.orchestrator
                    else "default",
                    "created_at": datetime.now(UTC).isoformat(),
                    "completed_at": None,
                }
                self._sessions_store.insert(record)
                return {
                    "ok": True,
                    "session_id": session_id,
                    "display_name": tc["label"],
                    "message": f"Session '{tc['label']}' created with {len(cards)} cards",
                }
        except Exception as e:
            logger.error("Failed to create session: %s", e)
            return {"ok": False, "message": str(e)}

        return {"ok": False, "message": "Could not create session"}

    def _complete_session(self, data: dict) -> dict:
        """Mark a session as completed."""
        session_id = data.get("session_id", "")
        if not session_id:
            return {"ok": False, "message": "session_id required"}
        try:
            s = self._sessions_store.get(session_id)
            if not s:
                return {"ok": False, "message": "Session not found"}
            from datetime import datetime

            self._sessions_store.update(
                session_id,
                {
                    "phase": "completed",
                    "completed_at": datetime.now(UTC).isoformat(),
                },
            )
            return {"ok": True, "message": "Session completed"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _save_card_answer(self, data: dict) -> dict:
        """Save the user's answer to a card (config/quiz)."""
        session_id = data.get("session_id", "")
        card_index = data.get("card_index", 0)
        answer = data.get("answer", "")
        if not session_id:
            return {"ok": False, "message": "session_id required"}
        try:
            s = self._sessions_store.get(session_id)
            if not s:
                return {"ok": False, "message": "Session not found"}
            cards = s.get("cards", [])
            if 0 <= card_index < len(cards):
                cards[card_index]["answer"] = answer
                self._sessions_store.update(
                    session_id, {"cards": cards, "card_index": card_index + 1}
                )
            return {"ok": True}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _config_approve(self, data: dict) -> dict:
        """L7: Accept or reject a config suggestion. On accept: apply to YAML and log to config-changelog."""
        session_id = data.get("session_id", "")
        card_index = data.get("card_index", 0)
        accepted = data.get("accepted", False)
        if not session_id:
            return {"ok": False, "message": "session_id required"}
        try:
            s = self._sessions_store.get(session_id)
            if not s:
                return {"ok": False, "message": "Session not found"}
            cards = s.get("cards", [])
            if not (0 <= card_index < len(cards)):
                return {"ok": False, "message": "Invalid card_index"}
            card = cards[card_index]
            if card.get("type") != "config":
                return {"ok": False, "message": "Not a config card"}
            config_file = card.get("config_file", "")
            field = card.get("field", "")
            proposed_value = card.get("proposed_value", "")
            current_value = card.get("current_value", "")
            reason = card.get("reason", "")
            list_op = card.get("list_operation", "")
            list_item = card.get("list_item", "")
            if accepted and config_file and field:
                from vastian.access_tiers import _find_project_root
                from vastian.storage.data_root import get_config_dir

                project_root = self._state_dir.parent
                if config_file.startswith("config/"):
                    config_path = get_config_dir(project_root) / config_file.removeprefix("config/")
                else:
                    config_path = _find_project_root() / config_file
                if not config_path.exists():
                    self._append_config_proposal_audit(
                        config_file,
                        field,
                        current_value,
                        proposed_value,
                        reason,
                        "rejected",
                        session_id,
                        list_op or None,
                        list_item or None,
                        f"Config file not found: {config_file}",
                    )
                    return {"ok": False, "message": f"Config file not found: {config_file}"}
                # Enum validation for config/ui-preferences.yaml (see docs/config-options-overview.md)
                if config_file == "config/ui-preferences.yaml" and not (list_op and list_item):
                    leaf = field.split(".")[-1]
                    valid = {
                        "theme": ("light", "dark", "system"),
                        "theme_preset": (
                            "default",
                            "ocean",
                            "forest",
                            "sunset",
                            "midnight",
                            "rose",
                            "slate",
                        ),
                        "density": ("compact", "normal", "spacious"),
                    }
                    if leaf in valid and proposed_value.lower() not in valid[leaf]:
                        msg = f"Invalid {leaf}. Valid values: {', '.join(valid[leaf])}"
                        self._append_config_proposal_audit(
                            config_file,
                            field,
                            current_value,
                            proposed_value,
                            reason,
                            "rejected",
                            session_id,
                            list_op or None,
                            list_item or None,
                            msg,
                        )
                        return {"ok": False, "message": msg}
                # Template var validation for agents/*.yaml prompt fields (skip for list-merge)
                if (
                    not (list_op and list_item)
                    and "agents" in config_file
                    and config_file.endswith(".yaml")
                ):
                    leaf = field.split(".")[-1]
                    if leaf in ("role_description", "system_prompt_extra", "tone"):
                        agent_id = config_path.stem
                        # Require {{user_name}} if current had it (non-finley)
                        if agent_id != "finley" and "{{user_name}}" in current_value:  # noqa: SIM102
                            if "{{user_name}}" not in proposed_value:
                                msg = "Proposed edit would remove required template variable. Preserve {{user_name}} and other {{...}} placeholders."
                                self._append_config_proposal_audit(
                                    config_file,
                                    field,
                                    current_value,
                                    proposed_value,
                                    reason,
                                    "rejected",
                                    session_id,
                                    list_op or None,
                                    list_item or None,
                                    msg,
                                )
                                return {"ok": False, "message": msg}
                        # Finley: require financial vars in system_prompt_extra if present
                        if agent_id == "finley" and leaf == "system_prompt_extra":
                            for var in ("{{mercury_account_directory}}", "{{financial_context}}"):
                                if var in current_value and var not in proposed_value:
                                    msg = "Proposed edit would remove required template variable. Preserve {{mercury_account_directory}} and {{financial_context}}."
                                    self._append_config_proposal_audit(
                                        config_file,
                                        field,
                                        current_value,
                                        proposed_value,
                                        reason,
                                        "rejected",
                                        session_id,
                                        list_op or None,
                                        list_item or None,
                                        msg,
                                    )
                                    return {"ok": False, "message": msg}
                import yaml

                with open(config_path, encoding="utf-8") as f:
                    cfg = yaml.safe_load(f) or {}
                keys = field.split(".")
                target = cfg
                for k in keys[:-1]:
                    if k not in target:
                        target[k] = {}
                    target = target[k]
                # List-merge semantics (Wave 9: responsibilities add/remove; scenario_boards add/remove)
                if list_op and list_item:
                    lst = target.get(keys[-1])
                    if not isinstance(lst, list):
                        lst = []
                    if list_op == "add":
                        # scenario_boards: list_item is JSON string of board object
                        if (
                            config_file == "config/boards/boards.yaml"
                            and keys[-1] == "scenario_boards"
                        ):
                            try:
                                new_board = json.loads(list_item)
                                if not isinstance(new_board, dict):
                                    msg = "scenario_boards add: list_item must be JSON object."
                                    self._append_config_proposal_audit(
                                        config_file,
                                        field,
                                        current_value,
                                        proposed_value,
                                        reason,
                                        "rejected",
                                        session_id,
                                        list_op,
                                        list_item,
                                        msg,
                                    )
                                    return {"ok": False, "message": msg}
                                bid = new_board.get("id", "")
                                if not bid or not str(bid).startswith("scenario-"):
                                    msg = "scenario_boards id must start with scenario-"
                                    self._append_config_proposal_audit(
                                        config_file,
                                        field,
                                        current_value,
                                        proposed_value,
                                        reason,
                                        "rejected",
                                        session_id,
                                        list_op,
                                        list_item,
                                        msg,
                                    )
                                    return {"ok": False, "message": msg}
                                for req in ("prefix", "label", "color", "scenario_id"):
                                    if req not in new_board:
                                        msg = f"scenario_boards requires {req}."
                                        self._append_config_proposal_audit(
                                            config_file,
                                            field,
                                            current_value,
                                            proposed_value,
                                            reason,
                                            "rejected",
                                            session_id,
                                            list_op,
                                            list_item,
                                            msg,
                                        )
                                        return {"ok": False, "message": msg}
                                # Dedupe by id
                                lst = [
                                    x
                                    for x in lst
                                    if (x.get("id") if isinstance(x, dict) else x) != bid
                                ]
                                lst.append(new_board)
                            except json.JSONDecodeError as e:
                                msg = f"scenario_boards add: invalid JSON: {e}"
                                self._append_config_proposal_audit(
                                    config_file,
                                    field,
                                    current_value,
                                    proposed_value,
                                    reason,
                                    "rejected",
                                    session_id,
                                    list_op,
                                    list_item,
                                    msg,
                                )
                                return {"ok": False, "message": msg}
                        elif list_item not in lst:
                            lst = lst + [list_item]
                        target[keys[-1]] = lst
                    elif list_op == "remove":
                        # scenario_boards: list_item is board id string
                        if (
                            config_file == "config/boards/boards.yaml"
                            and keys[-1] == "scenario_boards"
                        ):
                            target[keys[-1]] = [
                                x
                                for x in lst
                                if (x.get("id") if isinstance(x, dict) else x) != list_item
                            ]
                        else:
                            target[keys[-1]] = [x for x in lst if x != list_item]
                else:
                    val = proposed_value
                    if proposed_value.isdigit():
                        val = int(proposed_value)
                    elif proposed_value.lower() in ("true", "false"):
                        val = proposed_value.lower() == "true"
                    target[keys[-1]] = val
                with open(config_path, "w", encoding="utf-8") as f:
                    yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
                changelog_path = self._state_dir / "config-changelog.json"
                from datetime import datetime

                entry = {
                    "timestamp": datetime.now(UTC).isoformat(),
                    "config_file": config_file,
                    "field": field,
                    "previous_value": current_value,
                    "new_value": proposed_value,
                    "reason": reason,
                }
                try:
                    if changelog_path.exists():
                        changelog = json.loads(changelog_path.read_text(encoding="utf-8"))
                    else:
                        changelog = []
                    if not isinstance(changelog, list):
                        changelog = []
                    changelog.append(entry)
                    changelog_path.write_text(
                        json.dumps(changelog, indent=2, default=str), encoding="utf-8"
                    )
                except Exception as e:
                    logger.warning("Config changelog write failed: %s", e)
                self._append_config_proposal_audit(
                    config_file,
                    field,
                    current_value,
                    proposed_value,
                    reason,
                    "accepted",
                    session_id,
                    list_op or None,
                    list_item or None,
                )
            else:
                self._append_config_proposal_audit(
                    config_file,
                    field,
                    current_value,
                    proposed_value,
                    reason,
                    "rejected",
                    session_id,
                    list_op or None,
                    list_item or None,
                )
            cards[card_index]["answer"] = "accepted" if accepted else "rejected"
            self._sessions_store.update(session_id, {"cards": cards})
            return {"ok": True, "message": "Accepted and applied." if accepted else "Rejected."}
        except Exception as e:
            logger.exception("Config approve failed")
            return {"ok": False, "message": str(e)}

    # ── HTML rendering ────────────────────────────────

    def _render_html(self, project_root=None) -> str:
        """Build the full HTML page. Theme from config (vastian data path)."""
        from vastian.access_tiers import is_feature_allowed

        # Admin tab (only visible at admin tier)
        show_admin = is_feature_allowed("admin_panel", self.user_tier)
        admin_tab = (
            '<button class="tab-btn" onclick="switchTab(\'admin\')">Admin</button>'
            if show_admin
            else ""
        )

        if show_admin:
            admin_content = self._render_admin_html()
        else:
            admin_content = (
                '<div class="admin-locked">Admin panel requires admin tier access.</div>'
            )

        import time

        from vastian.ui_theme import get_theme_css

        return (
            ROSTER_HTML.replace("__THEME_CSS__", get_theme_css(project_root))
            .replace("__USER_TIER__", self.user_tier)
            .replace("__ADMIN_TAB__", admin_tab)
            .replace("__ADMIN_CONTENT__", admin_content)
            .replace("</title>", f"</title>\n<!-- build:{int(time.time())} -->")
        )

    def _render_admin_html(self) -> str:
        """Render admin panel content (tier overrides, model config, session toggles)."""
        # Load current access tiers config for display
        try:
            from vastian.access_tiers import VALID_TIERS, tier_summary

            tiers_html = ""
            for tier in VALID_TIERS:
                s = tier_summary(tier)
                features = ", ".join(s.get("features", []))
                disabled = ", ".join(s.get("disabled_agents", [])) or "none"
                tiers_html += (
                    f'<div class="admin-row">'
                    f"<label><strong>{tier}</strong> ({s.get('label', '')})</label>"
                    f'<span style="color:#8b949e;font-size:0.78em">Features: {features}</span>'
                    f"</div>"
                    f'<div class="admin-row">'
                    f'<label style="padding-left:16px">Disabled agents</label>'
                    f'<span style="color:#8b949e">{disabled}</span>'
                    f"</div>"
                )
        except Exception:
            tiers_html = '<div class="empty">Could not load tier config</div>'

        # Session types summary
        try:
            if self.session_engine and hasattr(self.session_engine, "get_session_types"):
                all_types = self.session_engine.get_session_types("admin")
                types_html = ""
                for t in all_types:
                    types_html += (
                        f'<div class="admin-row">'
                        f"<label>{t['label']} ({t['type_id']})</label>"
                        f'<span style="color:#8b949e">Lead: {t["lead"]} | Tier: {t["tier_required"]}</span>'
                        f"</div>"
                    )
            else:
                types_html = '<div class="empty">Session engine not connected</div>'
        except Exception:
            types_html = '<div class="empty">Could not load session types</div>'

        return (
            "<h2>Admin Panel</h2>"
            '<p class="subtitle">System configuration overview (read-only)</p>'
            '<div class="admin-section"><h3>Access Tiers</h3>' + tiers_html + "</div>"
            '<div class="admin-section"><h3>Session Types</h3>' + types_html + "</div>"
        )
