/**
 * Flynn Live Ops Dashboard
 *
 * Shows core counters, model performance, event stream, active requests,
 * and channel status. Fast metrics refresh every 3s, slow health every 10s.
 */

let _fastTimer = null;
let _slowTimer = null;
let _dashboardClient = null;
let _lastPlaybookRollbackPatches = null;
let _lastBriefingTestAt = null;
let _assistantSaveState = null;
let _lastAssistantConfig = null;
let _assistantManualOverrides = new Set();
let _assistantModelDefaultsDraft = null;
let _assistantDraftState = new Map();
let _assistantDraftTouchedAt = 0;
let _lastCouncilTask = '';
let _lastCouncilResult = null;
let _lastCouncilError = null;
let _lastServices = [];
let _lastLocalBackends = [];
let _lastDockerDependencies = [];
let _lastObservabilitySources = [];
let _lastObservabilitySeries = null;
let _lastServiceLogs = null;
let _selectedLogSourceId = null;
let _logRefreshTimer = null;
let _logViewerState = {
  lines: 200,
  sinceSeconds: 900,
  level: 'all',
  autoRefresh: true,
  paused: false,
  status: null,
  tone: 'neutral',
};
let _localBackendActionState = new Map();
let _dockerDependencyActionState = new Map();
let _serviceConfigState = {
  open: false,
  serviceName: null,
  status: null,
  tone: 'neutral',
  advancedPatch: '',
};

const MODEL_DEFAULT_TASK_KEYS = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning'];
const MODEL_DEFAULT_TIER_KEYS = ['default', 'fast', 'complex', 'local'];
const HEARTBEAT_CHECK_KEYS = ['gateway', 'model', 'channels', 'memory', 'disk', 'process_memory', 'backup', 'provider_errors'];
const LOCAL_BACKEND_ACTION_LABELS = {
  start: 'Start',
  restart: 'Restart',
  stop: 'Stop',
  update: 'Update',
};
const DOCKER_DEPENDENCY_ACTION_LABELS = {
  start: 'Start',
  restart: 'Restart',
  stop: 'Stop',
  update: 'Update',
};
const LOG_LEVEL_OPTIONS = ['all', 'info', 'warn', 'error'];
const LOG_LINES_OPTIONS = [100, 200, 500, 1000];
const LOG_SINCE_OPTIONS = [
  { value: 300, label: '5m' },
  { value: 900, label: '15m' },
  { value: 3600, label: '1h' },
  { value: 21600, label: '6h' },
];
const SERVICE_TOGGLE_PATCH_PATHS = {
  heartbeat: 'automation.heartbeat.enabled',
  daily_briefing: 'automation.daily_briefing.enabled',
  gmail: 'automation.gmail.enabled',
  gcal: 'automation.gcal.enabled',
  gdocs: 'automation.gdocs.enabled',
  gdrive: 'automation.gdrive.enabled',
  gtasks: 'automation.gtasks.enabled',
  backup: 'backup.enabled',
  audio_transcription: 'audio.enabled',
  sandbox: 'sandbox.enabled',
};
const ASSISTANT_DRAFT_TTL_MS = 2 * 60 * 1000;

function formatUptime(seconds) {
  const d = Math.floor(seconds / 86400);
  const h = Math.floor((seconds % 86400) / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  const parts = [];
  if (d > 0) {parts.push(`${d}d`);}
  if (h > 0) {parts.push(`${h}h`);}
  if (m > 0) {parts.push(`${m}m`);}
  parts.push(`${s}s`);
  return parts.join(' ');
}

function timeAgo(timestamp) {
  const secs = Math.floor((Date.now() - timestamp) / 1000);
  if (secs < 60) {return `${secs}s ago`;}
  if (secs < 3600) {return `${Math.floor(secs / 60)}m ago`;}
  return `${Math.floor(secs / 3600)}h ago`;
}

function formatTime(timestamp) {
  const d = new Date(timestamp);
  return d.toLocaleTimeString('en-GB', { hour12: false });
}

function formatDay(day) {
  const parsed = new Date(`${day}T00:00:00`);
  if (Number.isNaN(parsed.getTime())) {return day;}
  return parsed.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
}

function formatLogTime(timestamp) {
  if (!timestamp || !Number.isFinite(Number(timestamp))) {
    return '--:--:--';
  }
  return new Date(Number(timestamp)).toLocaleTimeString('en-GB', { hour12: false });
}

function formatNumber(value) {
  return (value ?? 0).toLocaleString();
}

function formatSessionDurationFromMessages(avgMessagesPerSession) {
  if (!avgMessagesPerSession || avgMessagesPerSession <= 0) {return '—';}
  return `${avgMessagesPerSession.toFixed(1)} msgs/session`;
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

function getAssistantStateSnapshot(configData) {
  const automation = configData?.automation ?? {};
  const memory = configData?.memory ?? {};
  const tts = configData?.tts ?? {};
  const queue = configData?.server?.queue ?? {};
  return {
    announce: (automation.delivery_mode ?? 'shared_session') === 'announce',
    dailyBriefing: Boolean(automation.daily_briefing?.enabled),
    memoryDaily: Boolean(memory.daily_log?.enabled),
    memoryProactive: Boolean(memory.proactive_extract?.enabled),
    memoryMinToolCalls: Number(memory.proactive_extract?.min_tool_calls ?? 1),
    ttsEnabled: Boolean(tts.enabled),
    ttsChannels: Array.isArray(tts.enabled_channels) ? tts.enabled_channels : [],
    queueMode: queue.mode ?? 'collect',
  };
}

function buildPlaybookPatches(playbook) {
  if (playbook === 'executive') {
    const patches = {
      'automation.delivery_mode': 'announce',
      'automation.daily_briefing.enabled': true,
      'memory.daily_log.enabled': true,
      'memory.proactive_extract.enabled': true,
      'memory.proactive_extract.min_tool_calls': 1,
      'tts.enabled': true,
      'tts.enabled_channels': [],
      'server.queue.mode': 'interrupt',
    };
    for (const key of _assistantManualOverrides) {
      delete patches[key];
    }
    return patches;
  }
  if (playbook === 'operator') {
    const patches = {
      'automation.delivery_mode': 'announce',
      'automation.daily_briefing.enabled': true,
      'memory.daily_log.enabled': true,
      'memory.proactive_extract.enabled': true,
      'memory.proactive_extract.min_tool_calls': 2,
      'tts.enabled': false,
      'server.queue.mode': 'steer_backlog',
    };
    for (const key of _assistantManualOverrides) {
      delete patches[key];
    }
    return patches;
  }
  const patches = {
    'automation.delivery_mode': 'shared_session',
    'automation.daily_briefing.enabled': false,
    'memory.daily_log.enabled': false,
    'memory.proactive_extract.enabled': false,
    'memory.proactive_extract.min_tool_calls': 3,
    'tts.enabled': false,
    'server.queue.mode': 'collect',
  };
  for (const key of _assistantManualOverrides) {
    delete patches[key];
  }
  return patches;
}

function buildRollbackPatchesFromSnapshot(snapshot) {
  return {
    'automation.delivery_mode': snapshot.announce ? 'announce' : 'shared_session',
    'automation.daily_briefing.enabled': snapshot.dailyBriefing,
    'memory.daily_log.enabled': snapshot.memoryDaily,
    'memory.proactive_extract.enabled': snapshot.memoryProactive,
    'memory.proactive_extract.min_tool_calls': Number.isFinite(snapshot.memoryMinToolCalls) ? snapshot.memoryMinToolCalls : 1,
    'tts.enabled': snapshot.ttsEnabled,
    'tts.enabled_channels': snapshot.ttsChannels,
    'server.queue.mode': snapshot.queueMode,
  };
}

function getByPath(obj, dottedPath) {
  if (!obj || typeof obj !== 'object') {return undefined;}
  const parts = dottedPath.split('.');
  let cursor = obj;
  for (const part of parts) {
    if (!cursor || typeof cursor !== 'object' || !(part in cursor)) {
      return undefined;
    }
    cursor = cursor[part];
  }
  return cursor;
}

function valuesMatch(expected, actual) {
  if (Array.isArray(expected) && Array.isArray(actual)) {
    if (expected.length !== actual.length) {return false;}
    for (let i = 0; i < expected.length; i++) {
      if (expected[i] !== actual[i]) {return false;}
    }
    return true;
  }
  return expected === actual;
}

function valuesMatchForPath(path, expected, actual) {
  if (valuesMatch(expected, actual)) {
    return true;
  }

  // Some optional string paths are normalized by config handlers:
  // writing "" is persisted as "unset" (undefined).
  if (typeof expected === 'string' && expected.trim().length === 0 && (actual === undefined || actual === null)) {
    const optionalStringSuffixes = ['scaffold_path'];
    if (optionalStringSuffixes.some((suffix) => path.endsWith(suffix))) {
      return true;
    }
  }

  return false;
}

function setAssistantSaveState(message, tone = 'neutral') {
  _assistantSaveState = {
    message,
    tone,
    at: Date.now(),
  };
}

function writeAssistantDraftValue(control) {
  if (!control || !control.id) {return;}
  const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
  if (isCheckbox) {
    _assistantDraftState.set(control.id, { kind: 'checkbox', value: Boolean(control.checked) });
  } else if (control.tagName === 'SELECT') {
    const selectedOption = Array.from(control.options ?? []).find((option) => option.selected);
    _assistantDraftState.set(control.id, { kind: 'value', value: selectedOption?.value ?? '' });
  } else {
    _assistantDraftState.set(control.id, { kind: 'value', value: control.value ?? '' });
  }
  _assistantDraftTouchedAt = Date.now();
}

function bindAssistantDraftTracking(rootEl) {
  const controls = rootEl.querySelectorAll('input[id], select[id], textarea[id]');
  controls.forEach((control) => {
    control.addEventListener('input', () => writeAssistantDraftValue(control));
    control.addEventListener('change', () => writeAssistantDraftValue(control));
  });
}

function applyAssistantDraftState(rootEl) {
  if (_assistantDraftState.size === 0) {return;}
  const now = Date.now();
  if (_assistantDraftTouchedAt > 0 && (now - _assistantDraftTouchedAt) > ASSISTANT_DRAFT_TTL_MS) {
    _assistantDraftState = new Map();
    _assistantDraftTouchedAt = 0;
    return;
  }

  for (const [id, draft] of _assistantDraftState.entries()) {
    const control = rootEl.querySelector(`#${id}`);
    if (!control) {continue;}
    const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
    if (draft.kind === 'checkbox' && isCheckbox) {
      control.checked = Boolean(draft.value);
    } else if (control.tagName === 'SELECT') {
      const options = Array.from(control.options ?? []);
      const desired = String(draft.value ?? '');
      let matchedIndex = -1;
      for (let i = 0; i < options.length; i++) {
        const option = options[i];
        const isSelected = option.value === desired;
        option.selected = isSelected;
        if (isSelected) {
          matchedIndex = i;
        }
      }
      if (options.length > 0) {
        const fallbackIndex = matchedIndex >= 0 ? matchedIndex : 0;
        options[fallbackIndex].selected = true;
        if ('selectedIndex' in control) {
          control.selectedIndex = fallbackIndex;
        }
      }
      try {
        control.value = desired;
      } catch {
        // Some DOM shims expose readonly value on select nodes.
      }
    } else if ('value' in control) {
      try {
        control.value = String(draft.value ?? '');
      } catch {
        // Ignore rare non-writable value surfaces from non-browser DOM shims.
      }
    }
  }
}

function renderAssistantSaveState() {
  if (!_assistantSaveState) {
    return '<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4">No recent save action.</div>';
  }

  const toneClass = _assistantSaveState.tone === 'success'
    ? 'text-green-500'
    : _assistantSaveState.tone === 'warning'
      ? 'text-amber-500'
      : _assistantSaveState.tone === 'error'
        ? 'text-red-500'
        : 'text-zinc-500';

  const at = new Date(_assistantSaveState.at).toLocaleTimeString(undefined, {
    hour: 'numeric',
    minute: '2-digit',
    second: '2-digit',
  });

  return `<div id="ops-assistant-status" class="text-sm ${toneClass} mt-4">${escapeHtml(_assistantSaveState.message)} <span class="text-zinc-500">(at ${escapeHtml(at)})</span></div>`;
}

function extractCouncilResultFromOutput(output) {
  if (typeof output !== 'string' || output.trim().length === 0) {
    return null;
  }
  const marker = '{"pipeline_version"';
  const idx = output.lastIndexOf(marker);
  if (idx < 0) {
    return null;
  }
  try {
    return JSON.parse(output.slice(idx));
  } catch {
    return null;
  }
}

// ── Initial full render ─────────────────────────────────────────

function renderSkeleton(el) {
  el.innerHTML = `
    <h1 class="text-2xl font-semibold text-zinc-50 mb-6">Live Ops Dashboard</h1>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Core Counters</h2>
    <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8" id="ops-counters">
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors"><div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Loading...</div><div class="text-2xl font-bold font-mono text-zinc-50">—</div></div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Model Performance</h2>
    <div id="ops-model-table">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Session Analytics</h2>
    <div id="ops-session-analytics">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Context Health</h2>
    <div id="ops-context-health">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Assistant Health</h2>
    <div id="ops-assistant-health">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Event Stream</h2>
    <div class="max-h-72 overflow-y-auto bg-zinc-900 border border-zinc-800 rounded-lg p-2 font-mono text-xs" id="ops-events">
      <div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">Loading events...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Active Requests</h2>
    <div id="ops-requests">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Services</h2>
    <div class="text-xs text-zinc-500 mb-2">Click a service card to configure.</div>
    <div id="ops-services" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Local LLM Backends</h2>
    <div class="text-xs text-zinc-500 mb-2">User-level daemon status and controls for local providers.</div>
    <div id="ops-local-backends" class="grid grid-cols-1 md:grid-cols-2 gap-3">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Docker Dependencies</h2>
    <div class="text-xs text-zinc-500 mb-2">Status for docker-compose services Flynn depends on (for example local Whisper transcription).</div>
    <div id="ops-docker-dependencies" class="grid grid-cols-1 md:grid-cols-2 gap-3">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Service Health Graphs</h2>
    <div class="text-xs text-zinc-500 mb-2">Bounded 1h trend view (30s buckets) for compose dependencies and systemd daemons.</div>
    <div id="ops-observability-graphs" class="grid grid-cols-1 md:grid-cols-2 gap-3">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>

    <h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Service Logs</h2>
    <div class="text-xs text-zinc-500 mb-2">Recent core service logs with server-side token masking.</div>
    <div id="ops-service-logs" class="bg-zinc-900 border border-zinc-800 rounded-lg p-3">
      <div class="text-sm text-zinc-500">Loading...</div>
    </div>
    <div id="ops-service-config-modal-root"></div>
  `;
}

// ── Section updaters (targeted DOM updates) ─────────────────────

function updateCounters(metrics, health) {
  const el = document.getElementById('ops-counters');
  if (!el) {return;}

  const sessions = health?.sessions ?? 0;
  const errCount = metrics?.errors ?? 0;

  const cards = [
    { label: 'Messages Processed', value: String(metrics?.messagesProcessed ?? 0), cls: '' },
    { label: 'Active Sessions', value: String(sessions), cls: '' },
    { label: 'Queue Depth', value: String(metrics?.queueDepth ?? 0), cls: '' },
    { label: 'Uptime', value: formatUptime(metrics?.uptime ?? 0), cls: '' },
    { label: 'Active Requests', value: String(metrics?.activeRequests ?? 0), cls: '' },
    { label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'text-red-500' : '' },
  ];

  el.innerHTML = cards.map(c =>
    `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
      <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">${c.label}</div>
      <div class="text-2xl font-bold font-mono text-zinc-50 ${c.cls}">${c.value}</div>
    </div>`,
  ).join('');
}

function updateModelTable(metrics) {
  const el = document.getElementById('ops-model-table');
  if (!el) {return;}

  const mc = metrics?.modelCalls;
  const calls = mc?.recentCalls ?? [];

  if (calls.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No model calls recorded yet</div>';
    return;
  }

  const totalCalls = mc.total ?? 0;
  const avgLatency = mc.avgLatency ?? 0;
  const errorRate = mc.errorRate ?? 0;

  const summaryHtml = `
    <div class="flex flex-wrap gap-4 md:gap-6 mb-3 text-sm text-zinc-400">
      <div><span>Total Calls:</span> <span class="font-mono text-zinc-50">${totalCalls}</span></div>
      <div><span>Avg Latency:</span> <span class="font-mono text-zinc-50">${avgLatency}ms</span></div>
      <div><span>Error Rate:</span> <span class="font-mono text-zinc-50">${(errorRate * 100).toFixed(2)}%</span></div>
    </div>
  `;

  // Show newest first
  const rows = [...calls].reverse().map(c => {
    const status = c.error ? '<span class="text-red-500">✗</span>' : '<span class="text-green-500">✓</span>';
    return `<tr class="hover:bg-zinc-800/50">
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(c.timestamp)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(c.provider)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.latency}ms</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.tokensPerSec.toFixed(1)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.inputTokens}/${c.outputTokens}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${status}</td>
    </tr>`;
  }).join('');

  el.innerHTML = `
    ${summaryHtml}
    <div class="overflow-x-auto">
      <table class="w-full text-sm">
        <thead>
          <tr>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Time</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Provider</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Latency</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Tokens/sec</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">In/Out</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Status</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function updateEvents(eventsData) {
  const el = document.getElementById('ops-events');
  if (!el) {return;}

  const events = eventsData?.events ?? [];

  if (events.length === 0) {
    el.innerHTML = '<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">No events recorded yet</div>';
    return;
  }

  // Events come newest-first from the API; show newest at bottom for log feel
  const reversed = [...events].reverse();

  el.innerHTML = reversed.map(e => {
    const time = formatTime(e.timestamp);
    const level = (e.level || 'info').toUpperCase();
    const levelColor = e.level === 'error' ? 'text-red-500' : e.level === 'warn' ? 'text-amber-500' : 'text-zinc-400';
    return `<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words ${levelColor}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
  }).join('');

  // Auto-scroll to bottom
  el.scrollTop = el.scrollHeight;
}

function updateActiveRequests(requestsData) {
  const el = document.getElementById('ops-requests');
  if (!el) {return;}

  const requests = requestsData?.requests ?? [];

  if (requests.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No active requests</div>';
    return;
  }

  const rows = requests.map(r => {
    const duration = r.durationMs < 1000
      ? `${r.durationMs}ms`
      : `${(r.durationMs / 1000).toFixed(1)}s`;
    const started = formatTime(r.startedAt);
    return `<tr class="hover:bg-zinc-800/50">
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(r.sessionId)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(r.channel)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${duration}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${started}</td>
    </tr>`;
  }).join('');

  el.innerHTML = `
    <div class="overflow-x-auto">
      <table class="w-full text-sm">
        <thead>
          <tr>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Channel</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Duration</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Started</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function updateSessionAnalytics(analyticsData) {
  const el = document.getElementById('ops-session-analytics');
  if (!el) {return;}

  const daily = analyticsData?.daily ?? [];
  const topSessions = analyticsData?.topSessions ?? [];
  const topTools = analyticsData?.topTools ?? [];
  const topTopics = analyticsData?.topTopics ?? [];

  const totalSessions = analyticsData?.totalSessions ?? 0;
  const totalMessages = analyticsData?.totalMessages ?? 0;
  const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;

  const summaryHtml = `
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions (Window)</div>
        <div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalSessions)}</div>
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Messages (Window)</div>
        <div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalMessages)}</div>
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Avg Session</div>
        <div class="text-2xl font-bold font-mono text-zinc-50">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
      </div>
    </div>
  `;

  const topToolsHtml = topTools.length > 0
    ? `<div>${topTools.map((tool) =>
      `<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(tool.toolName)}</span> <span class="text-zinc-500">(${formatNumber(tool.executions)})</span></div>`,
    ).join('')}</div>`
    : '<div class="text-sm text-zinc-500">No tool usage captured in this window</div>';

  const topTopicsHtml = topTopics.length > 0
    ? `<div>${topTopics.map((topic) =>
      `<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(topic.topic)}</span> <span class="text-zinc-500">(${formatNumber(topic.occurrences)})</span></div>`,
    ).join('')}</div>`
    : '<div class="text-sm text-zinc-500">No indexed topics captured in this window</div>';

  const topSessionsHtml = topSessions.length > 0
    ? `<div class="overflow-x-auto"><table class="w-full text-sm">
      <thead>
        <tr>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Last Active</th>
        </tr>
      </thead>
      <tbody>
        ${topSessions.map((session) => `
          <tr class="hover:bg-zinc-800/50">
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(session.sessionId)}</td>
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(session.messages)}</td>
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(session.lastActivity * 1000)}</td>
          </tr>
        `).join('')}
      </tbody>
    </table></div>`
    : '<div class="text-sm text-zinc-500">No session activity in this window</div>';

  const dailyHtml = daily.length > 0
    ? `<div class="overflow-x-auto"><table class="w-full text-sm">
      <thead>
        <tr>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Day</th>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Sessions</th>
          <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
        </tr>
      </thead>
      <tbody>
        ${daily.slice(0, 7).map((row) => `
          <tr class="hover:bg-zinc-800/50">
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatDay(row.day)}</td>
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.sessions)}</td>
            <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.messages)}</td>
          </tr>
        `).join('')}
      </tbody>
    </table></div>`
    : '<div class="text-sm text-zinc-500">No daily activity in this window</div>';

  el.innerHTML = `
    ${summaryHtml}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
        <div class="text-sm font-semibold text-zinc-50 mb-3">Top Tools</div>
        ${topToolsHtml}
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
        <div class="text-sm font-semibold text-zinc-50 mb-3">Top Topics</div>
        ${topTopicsHtml}
      </div>
    </div>
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
        <div class="text-sm font-semibold text-zinc-50 mb-3">Top Sessions</div>
        ${topSessionsHtml}
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
        <div class="text-sm font-semibold text-zinc-50 mb-3">Daily Trend (Last 7 Rows)</div>
        ${dailyHtml}
      </div>
    </div>
  `;
}

function updateContextHealth(contextData) {
  const el = document.getElementById('ops-context-health');
  if (!el) {return;}

  const sessions = contextData?.sessions ?? [];
  if (sessions.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No active context usage snapshots</div>';
    return;
  }

  const sorted = [...sessions].sort((a, b) => (b.budget?.usagePct ?? 0) - (a.budget?.usagePct ?? 0));
  const top = sorted.slice(0, 8);
  const highest = top[0]?.budget?.usagePct ?? 0;
  const overThreshold = sessions.filter(s => (s.budget?.shouldCompact ?? false)).length;

  const summary = `
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Highest Usage</div>
        <div class="text-2xl font-bold font-mono text-zinc-50 ${highest >= 90 ? 'text-red-500' : ''}">${highest.toFixed(1)}%</div>
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions Near Limit</div>
        <div class="text-2xl font-bold font-mono text-zinc-50 ${overThreshold > 0 ? 'text-red-500' : ''}">${overThreshold}</div>
      </div>
      <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
        <div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Active Snapshots</div>
        <div class="text-2xl font-bold font-mono text-zinc-50">${sessions.length}</div>
      </div>
    </div>
  `;

  const rows = top.map((entry) => {
    const budget = entry.budget ?? {};
    const usage = budget.usagePct ?? 0;
    const cls = usage >= 95 ? 'text-red-500' : usage >= 85 ? 'text-amber-500' : '';
    return `<tr class="hover:bg-zinc-800/50">
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(entry.sessionId)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono ${cls}">${usage.toFixed(1)}%</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)}</td>
      <td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${budget.shouldCompact ? 'yes' : 'no'}</td>
    </tr>`;
  }).join('');

  el.innerHTML = `
    ${summary}
    <div class="overflow-x-auto">
      <table class="w-full text-sm">
        <thead>
          <tr>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Usage</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Estimated Tokens</th>
            <th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Should Compact</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

async function applyAssistantPatch(patches, statusEl) {
  if (!_dashboardClient) {
    setAssistantSaveState('Save skipped: dashboard client unavailable.', 'error');
    return;
  }
  if (statusEl) {
    statusEl.textContent = 'Saving...';
    statusEl.className = 'text-sm text-zinc-500';
  }
  try {
    const result = await _dashboardClient.call('config.patch', { patches });
    const rejected = result?.rejected ?? [];
    const persistError = result?.persistError;
    const applied = result?.applied ?? [];
    const persisted = result?.persisted === true;

    let message = '';
    let tone = 'neutral';
    if (statusEl) {
      if (persistError) {
        message = `Save failed: ${persistError}`;
        tone = 'error';
      } else if (rejected.length > 0) {
        message = `Rejected: ${rejected.join(', ')}`;
        tone = 'error';
      } else if (!persisted) {
        message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`;
        tone = 'warning';
      } else {
        // Verify read-after-write so UI cannot claim persistence when value did not stick.
        try {
          const fresh = await _dashboardClient.call('config.get');
          const mismatches = [];
          for (const [key, value] of Object.entries(patches)) {
            const actual = getByPath(fresh, key);
            if (!valuesMatchForPath(key, value, actual)) {
              mismatches.push(`${key} expected=${JSON.stringify(value)} actual=${JSON.stringify(actual)}`);
            }
          }

          if (mismatches.length > 0) {
            message = `Saved response received but read-back mismatch: ${mismatches.join('; ')}`;
            tone = 'error';
          } else {
            message = `Saved to runtime + config file (${applied.length} updated)`;
            tone = 'success';
          }
        } catch (verifyError) {
          message = `Saved response received, but verification failed: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`;
          tone = 'warning';
        }
      }
      setAssistantSaveState(message, tone);
      statusEl.textContent = message;
      statusEl.className = `text-sm ${tone === 'success' ? 'text-green-500' : tone === 'warning' ? 'text-amber-500' : tone === 'error' ? 'text-red-500' : 'text-zinc-500'}`;
    }
    return { persisted, applied, rejected, persistError };
  } catch (error) {
    const msg = `Save error: ${error instanceof Error ? error.message : String(error)}`;
    setAssistantSaveState(msg, 'error');
    if (statusEl) {
      statusEl.textContent = msg;
      statusEl.className = 'text-sm text-red-500';
    }
    return { persisted: false, applied: [], rejected: [], persistError: msg };
  }
}

async function triggerDailyBriefingTest(jobName, statusEl) {
  if (!_dashboardClient) {return;}
  if (statusEl) {
    statusEl.textContent = 'Triggering test briefing...';
    statusEl.className = 'text-sm text-zinc-500';
  }
  try {
    const result = await _dashboardClient.call('tools.invoke', {
      tool: 'cron.trigger',
      args: { name: jobName },
    });

    if (result?.success) {
      const output = typeof result.output === 'string' ? result.output : 'Triggered.';
      if (statusEl) {
        statusEl.textContent = output;
        statusEl.className = 'text-sm text-green-500';
      }
      _lastBriefingTestAt = Date.now();
      return true;
    }

    if (statusEl) {
      statusEl.textContent = result?.error ?? 'Failed to trigger briefing.';
      statusEl.className = 'text-sm text-red-500';
    }
    return false;
  } catch (error) {
    if (statusEl) {
      statusEl.textContent = `Trigger error: ${error instanceof Error ? error.message : String(error)}`;
      statusEl.className = 'text-sm text-red-500';
    }
    return false;
  }
}

async function triggerCouncilRun(task, statusEl) {
  if (!_dashboardClient) {return false;}
  if (!task) {
    if (statusEl) {
      statusEl.textContent = 'Task is required.';
      statusEl.className = 'text-sm text-red-500';
    }
    return false;
  }
  if (statusEl) {
    statusEl.textContent = 'Running council...';
    statusEl.className = 'text-sm text-zinc-500';
  }
  try {
    const result = await _dashboardClient.call('tools.invoke', {
      tool: 'council.run',
      args: { task },
    });
    if (!result?.success) {
      _lastCouncilError = result?.error ?? 'Council run failed.';
      _lastCouncilResult = null;
      if (statusEl) {
        statusEl.textContent = _lastCouncilError;
        statusEl.className = 'text-sm text-red-500';
      }
      return false;
    }

    const parsed = extractCouncilResultFromOutput(result.output);
    if (!parsed) {
      _lastCouncilError = 'Council run succeeded but output could not be parsed.';
      _lastCouncilResult = null;
      if (statusEl) {
        statusEl.textContent = _lastCouncilError;
        statusEl.className = 'text-sm text-amber-500';
      }
      return false;
    }

    _lastCouncilTask = task;
    _lastCouncilResult = parsed;
    _lastCouncilError = null;
    if (statusEl) {
      statusEl.textContent = `Council run complete: ${parsed.stop_snapshot?.stop_reason ?? 'ok'}`;
      statusEl.className = 'text-sm text-green-500';
    }
    return true;
  } catch (error) {
    _lastCouncilError = `Council run error: ${error instanceof Error ? error.message : String(error)}`;
    _lastCouncilResult = null;
    if (statusEl) {
      statusEl.textContent = _lastCouncilError;
      statusEl.className = 'text-sm text-red-500';
    }
    return false;
  }
}

function updateAssistantHealth(configData) {
  const el = document.getElementById('ops-assistant-health');
  if (!el) {return;}
  _assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;

  const snapshot = getAssistantStateSnapshot(configData);

  const automation = configData?.automation ?? {};
  const memory = configData?.memory ?? {};
  const tts = configData?.tts ?? {};

  const deliveryMode = automation.delivery_mode ?? 'shared_session';
  const announce = deliveryMode === 'announce';
  const dailyBriefing = Boolean(automation.daily_briefing?.enabled);
  const memoryDaily = Boolean(memory.daily_log?.enabled);
  const memoryProactive = Boolean(memory.proactive_extract?.enabled);
  const proactiveThreshold = Number(memory.proactive_extract?.min_tool_calls ?? 1);
  const ttsEnabled = Boolean(tts.enabled);
  const briefing = automation.daily_briefing ?? {};
  const briefingName = briefing.name ?? 'daily-briefing';
  const briefingSchedule = briefing.schedule ?? '0 8 * * *';
  const briefingPrompt = briefing.prompt ?? '';
  const briefingOutput = briefing.output ?? null;
  const briefingModelTier = briefing.model_tier ?? 'default';
  const briefingTimezone = briefing.timezone ?? 'system';
  const briefingOutputLabel = briefingOutput?.channel && briefingOutput?.peer
    ? `${briefingOutput.channel}/${briefingOutput.peer}`
    : 'not configured';
  const briefingReady = dailyBriefing && Boolean(briefingOutput?.channel && briefingOutput?.peer);
  const playbookLikeReady = announce || (memoryDaily && memoryProactive);
  const modelTier = _assistantModelDefaultsDraft?.primaryTier ?? configData?.agents?.primary_tier ?? 'default';
  const delegation = configData?.agents?.delegation ?? {};
  const backgroundModels = configData?.agents?.background_models ?? {};
  const councils = configData?.councils ?? {};
  const councilsDefaults = councils.defaults ?? {};
  const councilsGroups = councils.groups ?? {};
  const councilsD = councilsGroups.D ?? {};
  const councilsP = councilsGroups.P ?? {};
  const tiers = configData?.models ?? {};
  const modelCatalog = configData?.__modelCatalog ?? [];
  const providerList = modelCatalog.length > 0
    ? modelCatalog.map((entry) => entry.provider)
    : ['anthropic', 'openai', 'gemini', 'openrouter', 'github', 'xai', 'ollama', 'llamacpp', 'bedrock', 'zhipuai', 'minimax', 'moonshot', 'synthetic'];
  const modelOptionsByProvider = Object.fromEntries(modelCatalog.map((entry) => [entry.provider, entry.models ?? []]));
  const checklistRows = [
    { label: 'Set briefing output channel + peer', done: Boolean(briefingOutput?.channel && briefingOutput?.peer) },
    { label: 'Enable assistant behavior profile', done: playbookLikeReady },
    { label: 'Send a test morning briefing', done: _lastBriefingTestAt !== null },
  ];

  const chip = (label, value) => `
    <div class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
      <span class="text-zinc-400">${escapeHtml(label)}</span>
      <span class="font-bold ${value ? 'text-green-500' : 'text-zinc-500'}">${value ? 'ON' : 'OFF'}</span>
    </div>
  `;
  const tierOption = (selected) => ['fast', 'default', 'complex', 'local']
    .map((tier) => `<option value="${tier}" ${selected === tier ? 'selected' : ''}>${tier}</option>`)
    .join('');
  const providerOption = (selected) => providerList
    .map((provider) => `<option value="${provider}" ${selected === provider ? 'selected' : ''}>${provider}</option>`)
    .join('');
  const modelDataList = (id, provider, selected) => {
    const options = modelOptionsByProvider[provider] ?? [];
    return `
      <input id="${id}" list="${id}-list" value="${escapeHtml(selected ?? '')}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Enter a model ID for the selected provider or pick one from the suggestion list." />
      <datalist id="${id}-list">
        ${options.map((model) => `<option value="${escapeHtml(model)}"></option>`).join('')}
      </datalist>
    `;
  };
  const taskRows = [
    { key: 'compaction', label: 'Compaction' },
    { key: 'memory_extraction', label: 'Memory extraction' },
    { key: 'classification', label: 'Classification' },
    { key: 'tool_summarisation', label: 'Tool summarisation' },
    { key: 'complex_reasoning', label: 'Complex reasoning' },
  ];
  const councilConversations = Array.isArray(_lastCouncilResult?.conversations) ? _lastCouncilResult.conversations : [];
  const councilSummary = _lastCouncilResult?.stop_snapshot
    ? `Last run: ${_lastCouncilResult.stop_snapshot.stop_reason} (round ${_lastCouncilResult.stop_snapshot.round_reached})`
    : (_lastCouncilError ? `Last run failed: ${_lastCouncilError}` : 'No council run yet in this dashboard session.');

  el.innerHTML = `
    <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 mb-4">
      ${chip('Announce Mode', announce)}
      ${chip('Daily Briefing', dailyBriefing)}
      ${chip('Memory Daily Log', memoryDaily)}
      ${chip('Proactive Extract', memoryProactive)}
      ${chip('TTS Replies', ttsEnabled)}
      <div class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
        <span class="text-zinc-400">Extract Threshold</span>
        <span class="font-bold">${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}</span>
      </div>
    </div>
    <div class="flex flex-wrap gap-2 mb-4">
      <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-announce">
        ${announce ? 'Disable Announce Mode' : 'Enable Announce Mode'}
      </button>
      <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-daily-briefing">
        ${dailyBriefing ? 'Disable Daily Briefing' : 'Enable Daily Briefing'}
      </button>
      <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-memory-daily">
        ${memoryDaily ? 'Disable Daily Log' : 'Enable Daily Log'}
      </button>
      <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-memory-proactive">
        ${memoryProactive ? 'Disable Proactive Extract' : 'Enable Proactive Extract'}
      </button>
      <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-tts">
        ${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
      </button>
    </div>
    <div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
      <div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Playbooks</div>
      <div class="grid grid-cols-2 md:grid-cols-4 gap-2 my-3">
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-executive">
          Executive
        </button>
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-operator">
          Operator
        </button>
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-focus">
          Focus
        </button>
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-undo" ${_lastPlaybookRollbackPatches ? '' : 'disabled'}>
          Undo Last Playbook
        </button>
      </div>
      <div class="text-sm text-zinc-500">Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.</div>
    </div>
    <div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
      <div class="text-sm font-semibold text-zinc-50 mb-3">Model Tier Defaults</div>
      <div class="text-sm text-zinc-500 mb-3">Tier provider/model definitions</div>
      <div class="space-y-3 mb-4">
        ${MODEL_DEFAULT_TIER_KEYS.map((tier) => {
    const cfg = tiers?.[tier] ?? {};
    const provider = _assistantModelDefaultsDraft?.tiers?.[tier]?.provider ?? cfg.provider ?? tiers?.default?.provider ?? 'openai';
    const model = _assistantModelDefaultsDraft?.tiers?.[tier]?.model ?? cfg.model ?? '';
    return `
            <div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
              <div class="text-sm text-zinc-50 mb-2">${escapeHtml(tier)} tier</div>
              <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
                <label class="flex flex-col gap-1" title="Provider used for this model tier.">
                  <span class="text-xs text-zinc-500">Provider</span>
                  <select id="assist-tier-${tier}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Select a provider for the ${escapeHtml(tier)} tier.">
                    ${providerOption(provider)}
                  </select>
                </label>
                <label class="flex flex-col gap-1" title="Default model name for this tier.">
                  <span class="text-xs text-zinc-500">Model</span>
                  ${modelDataList(`assist-tier-${tier}-model`, provider, model)}
                </label>
              </div>
            </div>
          `;
  }).join('')}
      </div>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
        <label class="flex flex-col gap-1.5" title="Main tier used when no task-specific routing rule applies.">
          <span class="text-sm text-zinc-400">Primary tier</span>
          <select id="assist-primary-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Choose the default model tier for general assistant responses.">
            ${tierOption(modelTier)}
          </select>
        </label>
      </div>
      <div class="text-sm text-zinc-400 mb-2">Delegation tiers + background model overrides</div>
      <div class="space-y-3">
        ${taskRows.map((task) => {
    const background = backgroundModels?.[task.key] ?? {};
    const draftTask = _assistantModelDefaultsDraft?.tasks?.[task.key] ?? {};
    const delegationTier = draftTask.delegationTier ?? delegation?.[task.key] ?? 'fast';
    const backgroundEnabled = draftTask.backgroundEnabled ?? Boolean(backgroundModels?.[task.key] && background?.enabled !== false);
    return `
            <div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
              <div class="text-sm text-zinc-50 mb-2">${escapeHtml(task.label)}</div>
              <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-2">
                <label class="flex flex-col gap-1" title="Tier used when this task is delegated.">
                  <span class="text-xs text-zinc-500">Delegation tier</span>
                  <select id="assist-delegation-${task.key}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Select the tier used for ${escapeHtml(task.label.toLowerCase())} delegation.">
                    ${tierOption(delegationTier)}
                  </select>
                </label>
                <label class="flex items-center gap-2 mt-5 md:mt-0" title="When enabled, this task can use a custom provider/model override.">
                  <input id="assist-bg-${task.key}-enabled" type="checkbox" ${backgroundEnabled ? 'checked' : ''} title="Toggle background provider/model override for this task." />
                  <span class="text-xs text-zinc-400">Enable provider/model override</span>
                </label>
                <label class="flex flex-col gap-1" title="Provider used when override is enabled.">
                  <span class="text-xs text-zinc-500">Provider</span>
                  <select id="assist-bg-${task.key}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Pick the override provider for this task.">
                    ${providerOption(draftTask.provider ?? background?.provider ?? tiers?.default?.provider ?? 'openai')}
                  </select>
                </label>
                <label class="flex flex-col gap-1" title="Model used when override is enabled.">
                  <span class="text-xs text-zinc-500">Model</span>
                  ${modelDataList(
    `assist-bg-${task.key}-model`,
    draftTask.provider ?? background?.provider ?? tiers?.default?.provider ?? 'openai',
    draftTask.model ?? background?.model ?? '',
  )}
                </label>
                <label class="flex flex-col gap-1" title="Tier to fall back to if the override model fails.">
                  <span class="text-xs text-zinc-500">Fallback tier</span>
                  <select id="assist-bg-${task.key}-fallback" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Choose the fallback tier for this task override.">
                    ${tierOption(draftTask.fallbackTier ?? background?.fallback_tier ?? 'fast')}
                  </select>
                </label>
              </div>
            </div>
          `;
  }).join('')}
      </div>
      <div class="flex flex-wrap gap-2 mt-3">
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-model-defaults">
          Save Model Defaults
        </button>
      </div>
    </div>
    <div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
      <div class="text-sm font-semibold text-zinc-50 mb-3">Councils</div>
      <div class="text-sm text-zinc-500 mb-3">On-demand council orchestration settings and council role model tiers.</div>
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
        <label class="flex items-center gap-2 mt-5 md:mt-0" title="Enable on-demand council orchestration flows.">
          <input id="assist-councils-enabled" type="checkbox" ${councils.enabled ? 'checked' : ''} title="Toggle council orchestration features." />
          <span class="text-xs text-zinc-400">Enable councils</span>
        </label>
        <label class="flex flex-col gap-1.5" title="Model tier for D-council agents.">
          <span class="text-sm text-zinc-400">D model tier</span>
          <select id="assist-council-d-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Set the model tier for D-council roles.">
            ${tierOption(councilsD.model_tier ?? 'complex')}
          </select>
        </label>
        <label class="flex flex-col gap-1.5" title="Model tier for P-council agents.">
          <span class="text-sm text-zinc-400">P model tier</span>
          <select id="assist-council-p-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Set the model tier for P-council roles.">
            ${tierOption(councilsP.model_tier ?? 'complex')}
          </select>
        </label>
        <label class="flex flex-col gap-1.5" title="Model tier used for council meta synthesis.">
          <span class="text-sm text-zinc-400">Meta model tier</span>
          <select id="assist-council-meta-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Set the model tier for the council meta arbiter.">
            ${tierOption(councils.meta_model_tier ?? 'complex')}
          </select>
        </label>
      </div>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
        <label class="flex flex-col gap-1.5" title="Agent ID for the D arbiter role.">
          <span class="text-sm text-zinc-400">D arbiter agent</span>
          <input id="assist-council-d-arbiter" type="text" value="${escapeHtml(councilsD.arbiter_agent ?? 'council_d_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Configured agent key for the D arbiter role." />
        </label>
        <label class="flex flex-col gap-1.5" title="Agent ID for the D freethinker role.">
          <span class="text-sm text-zinc-400">D freethinker agent</span>
          <input id="assist-council-d-freethinker" type="text" value="${escapeHtml(councilsD.freethinker_agent ?? 'council_d_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Configured agent key for the D freethinker role." />
        </label>
        <label class="flex flex-col gap-1.5" title="Agent ID for the P arbiter role.">
          <span class="text-sm text-zinc-400">P arbiter agent</span>
          <input id="assist-council-p-arbiter" type="text" value="${escapeHtml(councilsP.arbiter_agent ?? 'council_p_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Configured agent key for the P arbiter role." />
        </label>
        <label class="flex flex-col gap-1.5" title="Agent ID for the P freethinker role.">
          <span class="text-sm text-zinc-400">P freethinker agent</span>
          <input id="assist-council-p-freethinker" type="text" value="${escapeHtml(councilsP.freethinker_agent ?? 'council_p_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Configured agent key for the P freethinker role." />
        </label>
        <label class="flex flex-col gap-1.5" title="Agent ID for the meta arbiter role.">
          <span class="text-sm text-zinc-400">Meta arbiter agent</span>
          <input id="assist-council-meta-arbiter" type="text" value="${escapeHtml(councils.meta_arbiter_agent ?? 'council_meta_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Configured agent key for the council meta arbiter role." />
        </label>
        <label class="flex flex-col gap-1.5" title="Optional JSON scaffold used as council prompt template.">
          <span class="text-sm text-zinc-400">Scaffold path (optional)</span>
          <input id="assist-council-scaffold" type="text" value="${escapeHtml(councils.scaffold_path ?? '')}" placeholder="docs/councils/ai-council-production-scaffold.json" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Path to a scaffold JSON file under the project directory." />
        </label>
      </div>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
        <label class="flex flex-col gap-1.5" title="Upper bound on iterative council rounds per run.">
          <span class="text-sm text-zinc-400">Max rounds</span>
          <input id="assist-council-max-rounds" type="number" min="1" max="6" value="${escapeHtml(String(councilsDefaults.max_rounds ?? 2))}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Use 1-6 rounds; higher values increase cost and latency." />
        </label>
      </div>
      <div class="flex flex-wrap gap-2">
        <button type="button" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-councils">
          Save Councils
        </button>
      </div>
      <div class="mt-4 p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
        <div class="text-sm font-semibold text-zinc-50 mb-2">Council Conversations</div>
        <div class="text-xs text-zinc-500 mb-3">${escapeHtml(councilSummary)}</div>
        <div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-2 mb-3">
          <input id="assist-council-task" type="text" value="${escapeHtml(_lastCouncilTask)}" placeholder="Run councils on demand: e.g. design a 2-week experiment plan..." class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Prompt for an ad-hoc council run; use a concrete decision or planning question." />
          <button type="button" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="run-council">
            Run Council
          </button>
        </div>
        <div id="assist-council-status" class="text-sm text-zinc-500 mb-3"></div>
        <div class="max-h-72 overflow-y-auto space-y-2">
          ${councilConversations.length === 0 ? '<div class="text-sm text-zinc-500">No conversation log yet.</div>' : councilConversations.map((turn, idx) => `
                <details class="border border-zinc-800 rounded-md bg-zinc-900/70 p-2">
                  <summary class="cursor-pointer text-sm text-zinc-100">
                    #${idx + 1} ${escapeHtml(turn.call_id)} · ${escapeHtml(turn.agent)} @ ${escapeHtml(turn.tier)}
                  </summary>
                  <div class="mt-2 text-xs text-zinc-400">Prompt payload</div>
                  <pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(JSON.stringify(turn.prompt_payload, null, 2))}</pre>
                  <div class="mt-2 text-xs text-zinc-400">Response</div>
                  <pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(String(turn.response ?? ''))}</pre>
                </details>
              `).join('')}
        </div>
      </div>
    </div>
    <div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
      <div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Activation Checklist</div>
      <div class="space-y-1 mb-4">
        ${checklistRows.map((row) => `
          <div class="flex items-center gap-2 py-1 text-sm ${row.done ? 'text-zinc-50' : 'text-zinc-500'}">
            <span class="w-5 text-center font-bold">${row.done ? '✓' : '○'}</span>
            <span>${escapeHtml(row.label)}</span>
          </div>
        `).join('')}
      </div>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
        <label class="flex flex-col gap-1.5" title="Channel used for scheduled briefing messages.">
          <span class="text-sm text-zinc-400">Briefing output channel</span>
          <input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Set the output adapter name for daily briefing delivery." />
        </label>
        <label class="flex flex-col gap-1.5" title="Destination peer/chat ID for scheduled briefings.">
          <span class="text-sm text-zinc-400">Briefing output peer/chat id</span>
          <input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Set the recipient peer/chat ID for briefing delivery." />
        </label>
      </div>
      <div class="flex flex-wrap gap-2">
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-briefing-output">
          Save Briefing Output
        </button>
      </div>
    </div>
    <div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
      <div class="flex items-center justify-between mb-3">
        <div class="text-sm font-semibold text-zinc-50">Morning Brief Preview</div>
        <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
          Send Test Briefing
        </button>
      </div>
      <div class="flex flex-wrap gap-3 text-sm text-zinc-500 mb-3">
        <span>name: <code class="font-mono">${escapeHtml(briefingName)}</code></span>
        <span>schedule: <code class="font-mono">${escapeHtml(briefingSchedule)}</code></span>
        <span>timezone: <code class="font-mono">${escapeHtml(briefingTimezone)}</code></span>
        <span>tier: <code class="font-mono">${escapeHtml(briefingModelTier)}</code></span>
        <span>output: <code class="font-mono">${escapeHtml(briefingOutputLabel)}</code></span>
      </div>
      <div class="max-h-44 overflow-y-auto bg-zinc-950 border border-zinc-800 rounded-md p-3"><code class="text-sm text-zinc-400 font-mono whitespace-pre-wrap">${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
      ${briefingReady ? '' : '<div class="text-sm text-zinc-500 mt-2">Enable daily briefing and set output channel/peer to test-send.</div>'}
    </div>
    ${renderAssistantSaveState()}
  `;

  // Preserve local unsaved form edits across periodic dashboard refreshes.
  applyAssistantDraftState(el);

  const updateModelOptions = (inputId, provider) => {
    const input = el.querySelector(`#${inputId}`);
    const list = el.querySelector(`#${inputId}-list`);
    if (!input || !list) {return;}
    const options = modelOptionsByProvider[provider] ?? [];
    list.innerHTML = options.map((model) => `<option value="${escapeHtml(model)}"></option>`).join('');
  };

  const tierRows = ['default', 'fast', 'complex', 'local'];
  for (const tier of tierRows) {
    const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
    if (!providerSelect) {continue;}
    providerSelect.addEventListener('change', () => {
      updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
    });
  }

  const taskRowsForModels = MODEL_DEFAULT_TASK_KEYS;
  for (const task of taskRowsForModels) {
    const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
    if (!providerSelect) {continue;}
    providerSelect.addEventListener('change', () => {
      updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
    });
  }

  // Refresh datalist options after draft re-application in case provider selects were restored.
  for (const tier of tierRows) {
    const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
    if (!providerSelect) {continue;}
    updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
  }
  for (const task of taskRowsForModels) {
    const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
    if (!providerSelect) {continue;}
    updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
  }

  bindAssistantDraftTracking(el);

  const statusEl = el.querySelector('#ops-assistant-status');
  const buttons = el.querySelectorAll('.assistant-action-btn');
  buttons.forEach((button) => {
    button.addEventListener('click', async () => {
      const rawAction = button.getAttribute('data-action');
      const action = rawAction === 'save-council' ? 'save-councils' : rawAction;
      let patches = null;
      if (action === 'toggle-announce') {
        patches = { 'automation.delivery_mode': announce ? 'shared_session' : 'announce' };
        _assistantManualOverrides.add('automation.delivery_mode');
      } else if (action === 'toggle-daily-briefing') {
        patches = { 'automation.daily_briefing.enabled': !dailyBriefing };
        _assistantManualOverrides.add('automation.daily_briefing.enabled');
      } else if (action === 'toggle-memory-daily') {
        patches = { 'memory.daily_log.enabled': !memoryDaily };
        _assistantManualOverrides.add('memory.daily_log.enabled');
      } else if (action === 'toggle-memory-proactive') {
        patches = { 'memory.proactive_extract.enabled': !memoryProactive };
        _assistantManualOverrides.add('memory.proactive_extract.enabled');
      } else if (action === 'toggle-tts') {
        patches = { 'tts.enabled': !ttsEnabled };
        _assistantManualOverrides.add('tts.enabled');
      } else if (action === 'test-daily-briefing') {
        await triggerDailyBriefingTest(briefingName, statusEl);
      } else if (action === 'playbook-executive') {
        _lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
        patches = buildPlaybookPatches('executive');
      } else if (action === 'playbook-operator') {
        _lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
        patches = buildPlaybookPatches('operator');
      } else if (action === 'playbook-focus') {
        _lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
        patches = buildPlaybookPatches('focus');
      } else if (action === 'playbook-undo') {
        if (!_lastPlaybookRollbackPatches) {
          if (statusEl) {
            statusEl.textContent = 'No playbook changes to undo.';
            statusEl.className = 'text-sm text-zinc-500';
          }
          return;
        }
        patches = _lastPlaybookRollbackPatches;
        _lastPlaybookRollbackPatches = null;
      } else if (action === 'save-briefing-output') {
        const channel = (el.querySelector('#assist-brief-channel')?.value ?? '').trim();
        const peer = (el.querySelector('#assist-brief-peer')?.value ?? '').trim();
        if (!channel || !peer) {
          if (statusEl) {
            statusEl.textContent = 'Briefing output channel and peer are required.';
            statusEl.className = 'text-sm text-red-500';
          }
          return;
        }
        patches = {
          'automation.daily_briefing.output.channel': channel,
          'automation.daily_briefing.output.peer': peer,
          'automation.daily_briefing.enabled': true,
        };
        _assistantManualOverrides.add('automation.daily_briefing.enabled');
      } else if (action === 'save-model-defaults') {
        const tasks = MODEL_DEFAULT_TASK_KEYS;
        patches = {
          'agents.primary_tier': (el.querySelector('#assist-primary-tier')?.value ?? 'default'),
        };
        const tiers = MODEL_DEFAULT_TIER_KEYS;
        for (const tier of tiers) {
          const provider = (el.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
          const model = (el.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
          if (!provider || (!model && tier !== 'default')) {
            continue;
          }
          if (provider) {
            patches[`models.${tier}.provider`] = provider;
          }
          if (model) {
            patches[`models.${tier}.model`] = model;
          }
        }
        for (const task of tasks) {
          const delegationTier = el.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast';
          const enabled = Boolean(el.querySelector(`#assist-bg-${task}-enabled`)?.checked);
          const provider = (el.querySelector(`#assist-bg-${task}-provider`)?.value ?? '').trim();
          const model = (el.querySelector(`#assist-bg-${task}-model`)?.value ?? '').trim();
          const fallbackTier = el.querySelector(`#assist-bg-${task}-fallback`)?.value ?? 'fast';

          patches[`agents.delegation.${task}`] = delegationTier;
          patches[`agents.background_models.${task}.enabled`] = enabled;
          if (provider) {
            patches[`agents.background_models.${task}.provider`] = provider;
          }
          if (model) {
            patches[`agents.background_models.${task}.model`] = model;
          }
          patches[`agents.background_models.${task}.fallback_tier`] = fallbackTier;
        }
      } else if (action === 'save-councils') {
        const enabled = Boolean(el.querySelector('#assist-councils-enabled')?.checked);
        const dTier = el.querySelector('#assist-council-d-tier')?.value ?? 'complex';
        const pTier = el.querySelector('#assist-council-p-tier')?.value ?? 'complex';
        const metaTier = el.querySelector('#assist-council-meta-tier')?.value ?? 'complex';
        const dArbiter = (el.querySelector('#assist-council-d-arbiter')?.value ?? '').trim();
        const dFreethinker = (el.querySelector('#assist-council-d-freethinker')?.value ?? '').trim();
        const pArbiter = (el.querySelector('#assist-council-p-arbiter')?.value ?? '').trim();
        const pFreethinker = (el.querySelector('#assist-council-p-freethinker')?.value ?? '').trim();
        const metaArbiter = (el.querySelector('#assist-council-meta-arbiter')?.value ?? '').trim();
        const scaffoldPath = (el.querySelector('#assist-council-scaffold')?.value ?? '').trim();
        const maxRoundsRaw = Number(el.querySelector('#assist-council-max-rounds')?.value ?? 2);
        const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(6, Math.floor(maxRoundsRaw))) : 2;

        if (!dArbiter || !dFreethinker || !pArbiter || !pFreethinker || !metaArbiter) {
          if (statusEl) {
            statusEl.textContent = 'All council agent names are required.';
            statusEl.className = 'text-sm text-red-500';
          }
          return;
        }

        patches = {
          'councils.enabled': enabled,
          'councils.defaults.max_rounds': maxRounds,
          'councils.groups.D.model_tier': dTier,
          'councils.groups.P.model_tier': pTier,
          'councils.meta_model_tier': metaTier,
          'councils.groups.D.arbiter_agent': dArbiter,
          'councils.groups.D.freethinker_agent': dFreethinker,
          'councils.groups.P.arbiter_agent': pArbiter,
          'councils.groups.P.freethinker_agent': pFreethinker,
          'councils.meta_arbiter_agent': metaArbiter,
          'councils.scaffold_path': scaffoldPath,
        };
      } else if (action === 'run-council') {
        const councilTask = (el.querySelector('#assist-council-task')?.value ?? '').trim();
        const councilStatusEl = el.querySelector('#assist-council-status');
        const ok = await triggerCouncilRun(councilTask, councilStatusEl);
        if (ok && _lastAssistantConfig) {
          updateAssistantHealth(_lastAssistantConfig);
        }
        return;
      }
      if (!patches) {return;}
      const patchResult = await applyAssistantPatch(patches, statusEl);
      if (action === 'save-model-defaults' && patchResult.applied.length > 0 && patchResult.rejected.length === 0) {
        _assistantModelDefaultsDraft = null;
      } else if (action === 'save-model-defaults') {
        _assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;
      }
      // Force immediate refresh of slow sections after applying.
      const refreshed = await fetchSlow(_dashboardClient);
      if (refreshed) {
        if (refreshed.config) {
          _lastAssistantConfig = refreshed.config;
        }
        updateServices(refreshed.services);
        updateLocalBackends(refreshed.localBackends);
        updateDockerDependencies(refreshed.dockerDependencies);
        if (refreshed.observabilitySources) {
          _lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
            ? refreshed.observabilitySources.sources
            : [];
        }
        if (refreshed.observabilitySeries) {
          updateObservabilityGraphs(refreshed.observabilitySeries);
        }
        updateServiceLogsPanel();
        await refreshServiceLogs({ force: true });
        updateSessionAnalytics(refreshed.sessionAnalytics);
        updateContextHealth(refreshed.contextUsage);
        // Only re-render assistant controls from a confirmed config snapshot.
        if (refreshed.config) {
          updateAssistantHealth(_lastAssistantConfig);
        } else if (_lastAssistantConfig) {
          updateAssistantHealth(_lastAssistantConfig);
        }
      }
    });
  });
}

function readAssistantModelDefaultsDraft(rootEl) {
  const primaryTier = rootEl.querySelector('#assist-primary-tier')?.value;
  if (!primaryTier) {
    return null;
  }

  const tiers = {};
  for (const tier of MODEL_DEFAULT_TIER_KEYS) {
    const provider = (rootEl.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
    const model = (rootEl.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
    if (!provider && !model) {continue;}
    tiers[tier] = { provider, model };
  }

  const tasks = {};
  for (const task of MODEL_DEFAULT_TASK_KEYS) {
    tasks[task] = {
      delegationTier: rootEl.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast',
      backgroundEnabled: Boolean(rootEl.querySelector(`#assist-bg-${task}-enabled`)?.checked),
      provider: (rootEl.querySelector(`#assist-bg-${task}-provider`)?.value ?? '').trim(),
      model: (rootEl.querySelector(`#assist-bg-${task}-model`)?.value ?? '').trim(),
      fallbackTier: rootEl.querySelector(`#assist-bg-${task}-fallback`)?.value ?? 'fast',
    };
  }

  return {
    primaryTier,
    tiers,
    tasks,
  };
}

function _updateChannels(channelsData) {
  const el = document.getElementById('ops-channels');
  if (!el) {return;}

  const channels = channelsData?.channels ?? [];

  if (channels.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No channels registered</div>';
    return;
  }

  el.innerHTML = channels.map(ch =>
    `<div class="channel-card">
      <span class="channel-dot ${ch.status}"></span>
      <span class="channel-name">${escapeHtml(ch.name)}</span>
    </div>`,
  ).join('');
}

function updateServices(servicesData) {
  const el = document.getElementById('ops-services');
  if (!el) {return;}

  const services = servicesData?.services ?? [];
  _lastServices = services;

  if (services.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No services configured</div>';
    renderServiceConfigModal();
    return;
  }

  el.innerHTML = services.map(svc => {
    const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
    const borderColor = svc.status === 'connected'
      ? 'border-l-green-500'
      : svc.status === 'configured'
        ? 'border-l-blue-500'
        : svc.status === 'error'
          ? 'border-l-red-500'
          : 'border-l-zinc-600 opacity-60';
    const statusColor = svc.status === 'connected'
      ? 'text-green-500'
      : svc.status === 'configured'
        ? 'text-blue-500'
        : svc.status === 'error'
          ? 'text-red-500'
          : 'text-zinc-500';
    const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
    return `<button class="service-card text-left bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1 border-l-4 ${borderColor} hover:border-zinc-700 transition-colors" data-service-name="${escapeHtml(svc.name)}">
      <div class="flex items-center gap-2">
        <span class="text-sm shrink-0">${typeIcon}</span>
        <span class="text-sm font-semibold text-zinc-50">${escapeHtml(svc.name)}${itemCount}</span>
      </div>
      <span class="text-xs uppercase ${statusColor}">${escapeHtml(svc.status)}</span>
      <span class="text-xs text-zinc-500">${escapeHtml(svc.description)}</span>
      <span class="text-xs text-zinc-600">Click to configure</span>
      ${svc.error ? `<span class="text-xs text-red-400">Error: ${escapeHtml(String(svc.error))}</span>` : ''}
    </button>`;
  }).join('');

  el.querySelectorAll('.service-card').forEach((card) => {
    card.addEventListener('click', () => {
      const serviceName = card.getAttribute('data-service-name');
      if (!serviceName) {return;}
      _serviceConfigState.open = true;
      _serviceConfigState.serviceName = serviceName;
      _serviceConfigState.status = null;
      _serviceConfigState.tone = 'neutral';
      renderServiceConfigModal();
    });
  });

  renderServiceConfigModal();
}

function updateLocalBackends(localBackendsData) {
  const el = document.getElementById('ops-local-backends');
  if (!el) {return;}

  const backends = localBackendsData?.backends ?? [];
  _lastLocalBackends = backends;

  if (backends.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No local backends detected</div>';
    return;
  }

  el.innerHTML = backends.map((backend) => {
    const backendId = String(backend.id ?? '');
    const actionState = _localBackendActionState.get(backendId) ?? null;
    const isPending = Boolean(actionState?.pending);
    const toneClass = backend.activeState === 'active'
      ? 'text-green-500'
      : backend.activeState === 'failed'
        ? 'text-red-500'
        : 'text-zinc-400';
    const configuredText = backend.configured ? 'configured' : 'not configured';
    const configuredClass = backend.configured ? 'text-blue-400' : 'text-zinc-500';
    const pidText = backend.pid ? String(backend.pid) : '—';
    const unitFileText = backend.unitFileState || 'unknown';
    const loadText = backend.loadState || 'unknown';
    const resultText = backend.result || 'unknown';
    const availableActions = Array.isArray(backend.availableActions)
      ? backend.availableActions.filter((value) => ['start', 'restart', 'stop', 'update'].includes(String(value)))
      : [];
    const actionMessage = actionState?.message
      ? `<div class="text-xs ${actionState.tone === 'error' ? 'text-red-400' : actionState.tone === 'success' ? 'text-green-400' : 'text-zinc-400'}">${escapeHtml(String(actionState.message))}</div>`
      : '';

    const actionButtons = availableActions.length > 0
      ? availableActions.map((action) => {
        const key = String(action);
        const label = LOCAL_BACKEND_ACTION_LABELS[key] ?? key;
        return `<button class="local-backend-action-btn px-2 py-1 text-xs border border-zinc-700 rounded text-zinc-200 hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
          data-backend-id="${escapeHtml(backendId)}"
          data-action="${escapeHtml(key)}"
          ${isPending ? 'disabled' : ''}
          title="${escapeHtml(`${label} ${backend.name ?? backendId}`)}">${escapeHtml(label)}</button>`;
      }).join('')
      : '<span class="text-xs text-zinc-500">No actions available</span>';

    return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-2">
      <div class="flex items-center justify-between gap-2">
        <div class="text-sm font-semibold text-zinc-50">${escapeHtml(String(backend.name ?? backendId))}</div>
        <span class="text-xs uppercase ${toneClass}">${escapeHtml(String(backend.statusText ?? backend.activeState ?? 'unknown'))}</span>
      </div>
      <div class="text-xs text-zinc-500">Unit: <span class="font-mono text-zinc-400">${escapeHtml(String(backend.unit ?? 'unknown'))}</span></div>
      <div class="text-xs text-zinc-500">PID: <span class="font-mono text-zinc-400">${escapeHtml(pidText)}</span> · Load: <span class="font-mono text-zinc-400">${escapeHtml(loadText)}</span> · Result: <span class="font-mono text-zinc-400">${escapeHtml(resultText)}</span></div>
      <div class="text-xs ${configuredClass}">${escapeHtml(configuredText)} · unit file: ${escapeHtml(unitFileText)}</div>
      ${backend.error ? `<div class="text-xs text-red-400">Error: ${escapeHtml(String(backend.error))}</div>` : ''}
      ${actionMessage}
      <div class="flex flex-wrap gap-2">${actionButtons}</div>
    </div>`;
  }).join('');

  el.querySelectorAll('.local-backend-action-btn').forEach((button) => {
    button.addEventListener('click', async () => {
      const backendId = button.getAttribute('data-backend-id');
      const action = button.getAttribute('data-action');
      if (!backendId || !action) {return;}
      await handleLocalBackendAction(backendId, action);
    });
  });
}

function updateDockerDependencies(dockerDependenciesData) {
  const el = document.getElementById('ops-docker-dependencies');
  if (!el) {return;}

  const dependencies = dockerDependenciesData?.dependencies ?? [];
  _lastDockerDependencies = dependencies;

  if (dependencies.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No docker dependencies detected</div>';
    return;
  }

  el.innerHTML = dependencies.map((dependency) => {
    const dependencyId = String(dependency.id ?? '');
    const actionState = _dockerDependencyActionState.get(dependencyId) ?? null;
    const isPending = Boolean(actionState?.pending);
    const state = String(dependency.state ?? 'unknown');
    const health = String(dependency.health ?? 'unknown');
    const statusText = String(dependency.statusText ?? state);
    const configured = Boolean(dependency.configured);
    const configuredText = configured ? 'configured' : 'not configured';
    const configuredClass = configured ? 'text-blue-400' : 'text-zinc-500';
    const toneClass = state === 'running'
      ? 'text-green-500'
      : state === 'not-found'
        ? 'text-amber-500'
        : state === 'unknown'
          ? 'text-red-500'
          : 'text-zinc-400';
    const containerName = dependency.containerName ? String(dependency.containerName) : '—';
    const availableActions = Array.isArray(dependency.availableActions)
      ? dependency.availableActions.filter((value) => ['start', 'restart', 'stop', 'update'].includes(String(value)))
      : [];
    const actionMessage = actionState?.message
      ? `<div class="text-xs ${actionState.tone === 'error' ? 'text-red-400' : actionState.tone === 'success' ? 'text-green-400' : 'text-zinc-400'}">${escapeHtml(String(actionState.message))}</div>`
      : '';
    const actionButtons = availableActions.length > 0
      ? availableActions.map((action) => {
        const key = String(action);
        const label = DOCKER_DEPENDENCY_ACTION_LABELS[key] ?? key;
        return `<button class="docker-dependency-action-btn px-2 py-1 text-xs border border-zinc-700 rounded text-zinc-200 hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
          data-dependency-id="${escapeHtml(dependencyId)}"
          data-action="${escapeHtml(key)}"
          ${isPending ? 'disabled' : ''}
          title="${escapeHtml(`${label} ${dependency.name ?? dependencyId}`)}">${escapeHtml(label)}</button>`;
      }).join('')
      : '<span class="text-xs text-zinc-500">No actions available</span>';

    return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-2">
      <div class="flex items-center justify-between gap-2">
        <div class="text-sm font-semibold text-zinc-50">${escapeHtml(String(dependency.name ?? dependency.id ?? 'dependency'))}</div>
        <span class="text-xs uppercase ${toneClass}">${escapeHtml(statusText)}</span>
      </div>
      <div class="text-xs text-zinc-500">Compose service: <span class="font-mono text-zinc-400">${escapeHtml(String(dependency.service ?? 'unknown'))}</span></div>
      <div class="text-xs text-zinc-500">Container: <span class="font-mono text-zinc-400">${escapeHtml(containerName)}</span></div>
      <div class="text-xs text-zinc-500">State: <span class="font-mono text-zinc-400">${escapeHtml(state)}</span> · Health: <span class="font-mono text-zinc-400">${escapeHtml(health)}</span></div>
      <div class="text-xs ${configuredClass}">${escapeHtml(configuredText)}</div>
      ${dependency.error ? `<div class="text-xs text-red-400">Error: ${escapeHtml(String(dependency.error))}</div>` : ''}
      ${actionMessage}
      <div class="flex flex-wrap gap-2">${actionButtons}</div>
    </div>`;
  }).join('');

  el.querySelectorAll('.docker-dependency-action-btn').forEach((button) => {
    button.addEventListener('click', async () => {
      const dependencyId = button.getAttribute('data-dependency-id');
      const action = button.getAttribute('data-action');
      if (!dependencyId || !action) {return;}
      await handleDockerDependencyAction(dependencyId, action);
    });
  });
}

function getObservabilityName(sourceId) {
  const source = _lastObservabilitySources.find((entry) => entry.id === sourceId);
  return source?.name ?? sourceId;
}

function buildSparklinePath(points, key, width = 320, height = 56) {
  if (!Array.isArray(points) || points.length === 0) {
    return null;
  }

  const values = points.map((point) => Number(point?.[key] ?? 0));
  const min = Math.min(...values);
  const max = Math.max(...values);
  const denom = max === min ? 1 : (max - min);

  return values.map((value, idx) => {
    const x = values.length === 1 ? 0 : (idx / (values.length - 1)) * width;
    const y = height - (((value - min) / denom) * height);
    return `${idx === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`;
  }).join(' ');
}

function updateObservabilityGraphs(seriesData) {
  const el = document.getElementById('ops-observability-graphs');
  if (!el) {return;}

  const series = Array.isArray(seriesData?.series) ? seriesData.series : [];
  _lastObservabilitySeries = seriesData ?? null;

  if (series.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No service trend data available yet</div>';
    return;
  }

  const cards = [...series]
    .sort((a, b) => getObservabilityName(String(a.sourceId)).localeCompare(getObservabilityName(String(b.sourceId))))
    .map((entry) => {
      const sourceId = String(entry.sourceId ?? '');
      const source = _lastObservabilitySources.find((candidate) => candidate.id === sourceId);
      const name = source?.name ?? sourceId;
      const status = String(source?.status ?? 'unknown');
      const points = Array.isArray(entry.points) ? entry.points : [];
      const latest = points.length > 0 ? points[points.length - 1] : null;
      const first = points.length > 0 ? points[0] : null;
      const restartDelta = latest && first ? Math.max(0, Number(latest.restartCount ?? 0) - Number(first.restartCount ?? 0)) : 0;
      const errorDelta = latest && first ? Math.max(0, Number(latest.errorCount ?? 0) - Number(first.errorCount ?? 0)) : 0;

      const statePath = buildSparklinePath(points, 'stateCode');
      const healthPath = buildSparklinePath(points, 'healthCode');
      const statusColor = status === 'running'
        ? 'text-green-400'
        : status === 'degraded'
          ? 'text-amber-400'
          : status === 'stopped'
            ? 'text-zinc-400'
            : 'text-red-400';

      return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-2">
        <div class="flex items-center justify-between gap-2">
          <div class="text-sm font-semibold text-zinc-50">${escapeHtml(name)}</div>
          <span class="text-xs uppercase ${statusColor}">${escapeHtml(status)}</span>
        </div>
        <div class="text-xs text-zinc-500">Source: <span class="font-mono text-zinc-400">${escapeHtml(sourceId)}</span></div>
        <div class="text-xs text-zinc-500">Window deltas: <span class="font-mono text-zinc-400">restarts ${restartDelta}</span> · <span class="font-mono text-zinc-400">errors ${errorDelta}</span></div>
        <div class="bg-zinc-950 border border-zinc-800 rounded p-2">
          ${statePath
    ? `<svg viewBox="0 0 320 56" class="w-full h-14">
              <path d="${statePath}" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" />
              ${healthPath ? `<path d="${healthPath}" fill="none" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />` : ''}
            </svg>`
    : '<div class="text-xs text-zinc-500">No sample points yet</div>'}
          <div class="text-[11px] text-zinc-500 mt-1">Blue: state code · Green dashed: health code</div>
        </div>
      </div>`;
    });

  el.innerHTML = cards.join('');
}

function normalizeLogSourceSelection() {
  const logSources = _lastObservabilitySources.filter((source) => source.logCapable);
  if (logSources.length === 0) {
    _selectedLogSourceId = null;
    return [];
  }
  if (!_selectedLogSourceId || !logSources.some((source) => source.id === _selectedLogSourceId)) {
    _selectedLogSourceId = logSources[0].id;
  }
  return logSources;
}

function updateServiceLogsPanel() {
  const el = document.getElementById('ops-service-logs');
  if (!el) {return;}

  const logSources = normalizeLogSourceSelection();
  if (logSources.length === 0) {
    el.innerHTML = '<div class="text-sm text-zinc-500">No log-capable services currently available</div>';
    return;
  }

  const snapshot = _lastServiceLogs;
  const rawLines = Array.isArray(snapshot?.lines) ? snapshot.lines : [];
  const filterLevel = _logViewerState.level;
  const lines = rawLines.filter((line) => filterLevel === 'all' || String(line.level ?? 'info') === filterLevel);
  const statusToneClass = _logViewerState.tone === 'error'
    ? 'text-red-400'
    : _logViewerState.tone === 'success'
      ? 'text-green-400'
      : 'text-zinc-500';
  const redactedBadge = snapshot?.redacted
    ? '<span class="px-2 py-0.5 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20 text-xs">redacted</span>'
    : '<span class="px-2 py-0.5 rounded bg-zinc-800 text-zinc-400 border border-zinc-700 text-xs">raw-safe</span>';

  const linesHtml = lines.length > 0
    ? lines.map((entry) => {
      const level = String(entry.level ?? 'info');
      const levelClass = level === 'error' ? 'text-red-400' : level === 'warn' ? 'text-amber-400' : 'text-zinc-400';
      const ts = formatLogTime(entry.ts);
      return `<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words"><span class="font-mono text-zinc-500">[${escapeHtml(ts)}]</span> <span class="uppercase ${levelClass}">${escapeHtml(level)}</span> <span class="text-zinc-200">${escapeHtml(String(entry.text ?? ''))}</span></div>`;
    }).join('')
    : '<div class="px-2 py-1 text-zinc-500">No log lines for current filters</div>';

  el.innerHTML = `
    <div class="flex flex-wrap items-center gap-2 mb-3">
      <label class="text-xs text-zinc-400">Service</label>
      <select id="ops-log-source" class="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-100">
        ${logSources.map((source) => `<option value="${escapeHtml(source.id)}" ${source.id === _selectedLogSourceId ? 'selected' : ''}>${escapeHtml(source.name)}</option>`).join('')}
      </select>
      <label class="text-xs text-zinc-400">Window</label>
      <select id="ops-log-since" class="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-100">
        ${LOG_SINCE_OPTIONS.map((opt) => `<option value="${opt.value}" ${Number(_logViewerState.sinceSeconds) === opt.value ? 'selected' : ''}>${opt.label}</option>`).join('')}
      </select>
      <label class="text-xs text-zinc-400">Lines</label>
      <select id="ops-log-lines-select" class="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-100">
        ${LOG_LINES_OPTIONS.map((opt) => `<option value="${opt}" ${Number(_logViewerState.lines) === opt ? 'selected' : ''}>${opt}</option>`).join('')}
      </select>
      <label class="text-xs text-zinc-400">Level</label>
      <select id="ops-log-level" class="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-100">
        ${LOG_LEVEL_OPTIONS.map((opt) => `<option value="${opt}" ${_logViewerState.level === opt ? 'selected' : ''}>${opt}</option>`).join('')}
      </select>
      <label class="text-xs text-zinc-300 flex items-center gap-1"><input id="ops-log-auto" type="checkbox" ${_logViewerState.autoRefresh ? 'checked' : ''}/>auto</label>
      <label class="text-xs text-zinc-300 flex items-center gap-1"><input id="ops-log-pause" type="checkbox" ${_logViewerState.paused ? 'checked' : ''}/>pause</label>
      <button id="ops-log-refresh" class="px-2 py-1 text-xs border border-zinc-700 rounded text-zinc-200 hover:bg-zinc-800">Refresh</button>
      ${redactedBadge}
    </div>
    <div class="flex items-center justify-between mb-2">
      <div class="text-xs text-zinc-500">${snapshot?.fetchedAt ? `Last fetch ${escapeHtml(formatLogTime(snapshot.fetchedAt))}` : 'No logs fetched yet'}</div>
      <div class="text-xs ${statusToneClass}">${escapeHtml(String(_logViewerState.status ?? ''))}</div>
    </div>
    <div id="ops-log-lines-view" class="max-h-80 overflow-y-auto bg-zinc-950 border border-zinc-800 rounded p-2 font-mono text-xs">${linesHtml}</div>
  `;

  el.querySelector('#ops-log-source')?.addEventListener('change', async (event) => {
    _selectedLogSourceId = event.target.value;
    await refreshServiceLogs({ force: true });
  });

  el.querySelector('#ops-log-since')?.addEventListener('change', async (event) => {
    const parsed = Number(event.target.value);
    if (Number.isFinite(parsed) && parsed > 0) {
      _logViewerState.sinceSeconds = parsed;
    }
    await refreshServiceLogs({ force: true });
  });

  el.querySelector('#ops-log-lines-select')?.addEventListener('change', async (event) => {
    const parsed = Number(event.target.value);
    if (Number.isFinite(parsed) && parsed > 0) {
      _logViewerState.lines = parsed;
    }
    await refreshServiceLogs({ force: true });
  });

  el.querySelector('#ops-log-level')?.addEventListener('change', (event) => {
    _logViewerState.level = event.target.value;
    updateServiceLogsPanel();
  });

  el.querySelector('#ops-log-auto')?.addEventListener('change', (event) => {
    _logViewerState.autoRefresh = Boolean(event.target.checked);
    if (_logViewerState.autoRefresh && !_logViewerState.paused) {
      void refreshServiceLogs({ force: true });
    }
  });

  el.querySelector('#ops-log-pause')?.addEventListener('change', (event) => {
    _logViewerState.paused = Boolean(event.target.checked);
  });

  el.querySelector('#ops-log-refresh')?.addEventListener('click', async () => {
    await refreshServiceLogs({ force: true });
  });
}

async function refreshServiceLogs(opts = {}) {
  if (!_dashboardClient || !_selectedLogSourceId) {return;}

  const force = opts.force === true;
  if (!force && (!_logViewerState.autoRefresh || _logViewerState.paused)) {
    return;
  }

  try {
    const result = await _dashboardClient.call('system.serviceLogs', {
      sourceId: _selectedLogSourceId,
      lines: _logViewerState.lines,
      sinceSeconds: _logViewerState.sinceSeconds,
    });
    _lastServiceLogs = result;
    _logViewerState.status = `Fetched ${Array.isArray(result?.lines) ? result.lines.length : 0} line(s)`;
    _logViewerState.tone = 'success';
  } catch (error) {
    _logViewerState.status = `Log fetch failed: ${error instanceof Error ? error.message : String(error)}`;
    _logViewerState.tone = 'error';
  }

  updateServiceLogsPanel();
}

async function handleLocalBackendAction(backendId, action) {
  if (!_dashboardClient) {return;}
  const actionLabel = LOCAL_BACKEND_ACTION_LABELS[action] ?? action;
  _localBackendActionState.set(backendId, { pending: true, tone: 'neutral', message: `${actionLabel} requested…` });
  updateLocalBackends({ backends: _lastLocalBackends });

  try {
    const result = await _dashboardClient.call('system.localBackendControl', {
      backend: backendId,
      action,
    });
    const status = result?.status;
    const resultMessage = typeof result?.message === 'string' ? result.message : null;
    if (status && typeof status === 'object') {
      _lastLocalBackends = _lastLocalBackends.map((backend) =>
        backend.id === backendId ? status : backend);
    }
    _localBackendActionState.set(backendId, {
      pending: false,
      tone: 'success',
      message: resultMessage ? `${actionLabel} completed: ${resultMessage}` : `${actionLabel} completed`,
    });
    updateLocalBackends({ backends: _lastLocalBackends });

    const refreshed = await fetchSlow(_dashboardClient);
    if (refreshed?.localBackends) {
      updateLocalBackends(refreshed.localBackends);
    }
    if (refreshed?.dockerDependencies) {
      updateDockerDependencies(refreshed.dockerDependencies);
    }
    if (refreshed?.observabilitySources) {
      _lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
        ? refreshed.observabilitySources.sources
        : [];
      updateServiceLogsPanel();
    }
    if (refreshed?.observabilitySeries) {
      updateObservabilityGraphs(refreshed.observabilitySeries);
    }
    await refreshServiceLogs({ force: true });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    _localBackendActionState.set(backendId, {
      pending: false,
      tone: 'error',
      message: `${actionLabel} failed: ${message}`,
    });
    updateLocalBackends({ backends: _lastLocalBackends });
  }
}

async function handleDockerDependencyAction(dependencyId, action) {
  if (!_dashboardClient) {return;}
  const actionLabel = DOCKER_DEPENDENCY_ACTION_LABELS[action] ?? action;
  _dockerDependencyActionState.set(dependencyId, { pending: true, tone: 'neutral', message: `${actionLabel} requested…` });
  updateDockerDependencies({ dependencies: _lastDockerDependencies });

  try {
    const result = await _dashboardClient.call('system.dockerDependencyControl', {
      dependency: dependencyId,
      action,
    });
    const status = result?.status;
    const resultMessage = typeof result?.message === 'string' ? result.message : null;
    if (status && typeof status === 'object') {
      _lastDockerDependencies = _lastDockerDependencies.map((dependency) =>
        dependency.id === dependencyId ? status : dependency);
    }
    _dockerDependencyActionState.set(dependencyId, {
      pending: false,
      tone: 'success',
      message: resultMessage ? `${actionLabel} completed: ${resultMessage}` : `${actionLabel} completed`,
    });
    updateDockerDependencies({ dependencies: _lastDockerDependencies });

    const refreshed = await fetchSlow(_dashboardClient);
    if (refreshed?.localBackends) {
      updateLocalBackends(refreshed.localBackends);
    }
    if (refreshed?.dockerDependencies) {
      updateDockerDependencies(refreshed.dockerDependencies);
    }
    if (refreshed?.observabilitySources) {
      _lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
        ? refreshed.observabilitySources.sources
        : [];
      updateServiceLogsPanel();
    }
    if (refreshed?.observabilitySeries) {
      updateObservabilityGraphs(refreshed.observabilitySeries);
    }
    await refreshServiceLogs({ force: true });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    _dockerDependencyActionState.set(dependencyId, {
      pending: false,
      tone: 'error',
      message: `${actionLabel} failed: ${message}`,
    });
    updateDockerDependencies({ dependencies: _lastDockerDependencies });
  }
}

function getConfigValue(path, fallbackValue) {
  const value = getByPath(_lastAssistantConfig, path);
  return value === undefined ? fallbackValue : value;
}

function renderServiceConfigModal() {
  const root = document.getElementById('ops-service-config-modal-root');
  if (!root) {return;}
  if (!_serviceConfigState.open || !_serviceConfigState.serviceName) {
    root.innerHTML = '';
    return;
  }

  const service = _lastServices.find((svc) => svc.name === _serviceConfigState.serviceName);
  if (!service) {
    _serviceConfigState.open = false;
    root.innerHTML = '';
    return;
  }

  const quickTogglePath = SERVICE_TOGGLE_PATCH_PATHS[service.name];
  const hasQuickToggle = Boolean(quickTogglePath);
  const quickToggleValue = hasQuickToggle ? Boolean(getConfigValue(quickTogglePath, false)) : false;
  const heartbeatSection = service.name === 'heartbeat'
    ? `
      <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
        <label class="flex flex-col gap-1" title="Heartbeat check interval (examples: 1m, 5m, 30s).">
          <span class="text-xs text-zinc-500">Interval (e.g. 5m)</span>
          <input id="svc-heartbeat-interval" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.interval', '5m')))}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="How often heartbeat checks are executed." />
        </label>
        <label class="flex flex-col gap-1" title="Minimum wait between repeated failure notifications.">
          <span class="text-xs text-zinc-500">Notify cooldown</span>
          <input id="svc-heartbeat-notify-cooldown" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.notify_cooldown', '30m')))}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Cooldown duration before sending another alert for the same issue." />
        </label>
        <label class="flex flex-col gap-1" title="Consecutive failed checks required before a service is marked unhealthy.">
          <span class="text-xs text-zinc-500">Failure threshold</span>
          <input id="svc-heartbeat-failure-threshold" type="number" min="1" max="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.failure_threshold', 2)))}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Use 1 for aggressive alerting, higher values to reduce false positives." />
        </label>
        <label class="flex flex-col gap-1" title="Free disk space alert threshold in megabytes.">
          <span class="text-xs text-zinc-500">Disk threshold (MB)</span>
          <input id="svc-heartbeat-disk-threshold" type="number" min="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.disk_threshold_mb', 100)))}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" title="Alert when free disk falls below this value." />
        </label>
      </div>
      <div class="mt-2">
        <div class="text-xs text-zinc-500 mb-1">Checks</div>
        <div class="grid grid-cols-2 md:grid-cols-4 gap-1">
          ${HEARTBEAT_CHECK_KEYS.map((check) => {
    const selected = Array.isArray(getConfigValue('automation.heartbeat.checks', HEARTBEAT_CHECK_KEYS))
      && getConfigValue('automation.heartbeat.checks', HEARTBEAT_CHECK_KEYS).includes(check);
    return `
              <label class="flex items-center gap-2 text-xs text-zinc-300" title="Enable or disable the ${escapeHtml(check)} heartbeat check.">
                <input type="checkbox" data-heartbeat-check="${check}" ${selected ? 'checked' : ''} title="Toggle ${escapeHtml(check)} check." />
                <span>${escapeHtml(check)}</span>
              </label>
            `;
  }).join('')}
        </div>
      </div>
    `
    : '';

  const toneClass = _serviceConfigState.tone === 'success'
    ? 'text-green-500'
    : _serviceConfigState.tone === 'error'
      ? 'text-red-500'
      : 'text-zinc-500';

  root.innerHTML = `
    <div id="svc-config-overlay" class="fixed inset-0 bg-black/60 z-40"></div>
    <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
      <div class="w-full max-w-3xl bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-xl">
        <div class="flex items-start justify-between gap-4 mb-3">
          <div>
            <div class="text-lg font-semibold text-zinc-50">Configure ${escapeHtml(service.name)}</div>
            <div class="text-xs text-zinc-400">${escapeHtml(service.description ?? '')}</div>
          </div>
          <button id="svc-config-close" class="px-2 py-1 text-xs border border-zinc-700 rounded text-zinc-300 hover:bg-zinc-800">Close</button>
        </div>
        <div class="mb-3 p-3 border border-zinc-800 rounded bg-zinc-950/60">
          <div class="text-xs uppercase text-zinc-500 mb-2">Quick Settings</div>
          ${hasQuickToggle ? `
            <label class="flex items-center gap-2 mb-2" title="Master on/off for this service (when available).">
              <input id="svc-quick-enabled" type="checkbox" ${quickToggleValue ? 'checked' : ''} title="Enable or disable this service." />
              <span class="text-sm text-zinc-200">Enabled</span>
            </label>
          ` : '<div class="text-xs text-zinc-500 mb-2">No quick toggle available for this service.</div>'}
          ${heartbeatSection}
        </div>
        <div class="mb-3 p-3 border border-zinc-800 rounded bg-zinc-950/60">
          <div class="text-xs uppercase text-zinc-500 mb-2">Advanced Patch (optional JSON)</div>
          <textarea id="svc-advanced-patch" rows="5" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm font-mono focus:border-blue-500 outline-none" placeholder='{"automation.heartbeat.enabled": true}' title="Optional JSON object of config path/value pairs to patch on save.">${escapeHtml(_serviceConfigState.advancedPatch ?? '')}</textarea>
        </div>
        <div class="flex items-center justify-between gap-2">
          <div id="svc-config-status" class="text-xs ${toneClass}">${escapeHtml(_serviceConfigState.status ?? '')}</div>
          <div class="flex items-center gap-2">
            <button id="svc-config-cancel" class="px-3 py-1.5 text-sm border border-zinc-700 rounded text-zinc-300 hover:bg-zinc-800">Cancel</button>
            <button id="svc-config-save" class="px-3 py-1.5 text-sm border border-zinc-700 rounded bg-zinc-800 text-zinc-100 hover:bg-zinc-700">Save</button>
          </div>
        </div>
      </div>
    </div>
  `;

  const closeModal = () => {
    _serviceConfigState.open = false;
    renderServiceConfigModal();
  };
  root.querySelector('#svc-config-close')?.addEventListener('click', closeModal);
  root.querySelector('#svc-config-cancel')?.addEventListener('click', closeModal);
  root.querySelector('#svc-config-overlay')?.addEventListener('click', closeModal);

  root.querySelector('#svc-config-save')?.addEventListener('click', async () => {
    if (!_dashboardClient || !_serviceConfigState.serviceName) {return;}
    const patches = {};

    if (quickTogglePath) {
      patches[quickTogglePath] = Boolean(root.querySelector('#svc-quick-enabled')?.checked);
    }
    if (_serviceConfigState.serviceName === 'heartbeat') {
      patches['automation.heartbeat.interval'] = (root.querySelector('#svc-heartbeat-interval')?.value ?? '5m').trim();
      patches['automation.heartbeat.notify_cooldown'] = (root.querySelector('#svc-heartbeat-notify-cooldown')?.value ?? '30m').trim();
      patches['automation.heartbeat.failure_threshold'] = Number(root.querySelector('#svc-heartbeat-failure-threshold')?.value ?? 2);
      patches['automation.heartbeat.disk_threshold_mb'] = Number(root.querySelector('#svc-heartbeat-disk-threshold')?.value ?? 100);
      patches['automation.heartbeat.checks'] = HEARTBEAT_CHECK_KEYS.filter(
        (check) => Boolean(root.querySelector(`[data-heartbeat-check="${check}"]`)?.checked),
      );
    }

    const advancedRaw = (root.querySelector('#svc-advanced-patch')?.value ?? '').trim();
    _serviceConfigState.advancedPatch = advancedRaw;
    if (advancedRaw.length > 0) {
      try {
        const parsed = JSON.parse(advancedRaw);
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
          throw new Error('Advanced patch must be a JSON object');
        }
        Object.assign(patches, parsed);
      } catch (error) {
        _serviceConfigState.status = error instanceof Error ? error.message : String(error);
        _serviceConfigState.tone = 'error';
        renderServiceConfigModal();
        return;
      }
    }

    if (Object.keys(patches).length === 0) {
      _serviceConfigState.status = 'No changes to save.';
      _serviceConfigState.tone = 'neutral';
      renderServiceConfigModal();
      return;
    }

    try {
      const result = await _dashboardClient.call('config.patch', { patches });
      const applied = Array.isArray(result?.applied) ? result.applied : [];
      const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
      if (applied.length === 0) {
        _serviceConfigState.status = rejected.length > 0
          ? `No changes applied. Rejected: ${rejected.join(', ')}`
          : 'No changes were applied.';
        _serviceConfigState.tone = 'error';
        renderServiceConfigModal();
        return;
      }
      _serviceConfigState.status = `Saved ${applied.length} setting(s).${rejected.length > 0 ? ` Rejected: ${rejected.join(', ')}` : ''}`;
      _serviceConfigState.tone = rejected.length > 0 ? 'error' : 'success';

      const refreshed = await fetchSlow(_dashboardClient);
      if (refreshed) {
        updateServices(refreshed.services);
        updateLocalBackends(refreshed.localBackends);
        updateDockerDependencies(refreshed.dockerDependencies);
        if (refreshed.observabilitySources) {
          _lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
            ? refreshed.observabilitySources.sources
            : [];
        }
        if (refreshed.observabilitySeries) {
          updateObservabilityGraphs(refreshed.observabilitySeries);
        }
        updateServiceLogsPanel();
        await refreshServiceLogs({ force: true });
        updateSessionAnalytics(refreshed.sessionAnalytics);
        updateContextHealth(refreshed.contextUsage);
        if (refreshed.config) {
          _lastAssistantConfig = refreshed.config;
          updateAssistantHealth(_lastAssistantConfig);
        }
      }
      renderServiceConfigModal();
    } catch (error) {
      _serviceConfigState.status = `Save failed: ${error instanceof Error ? error.message : String(error)}`;
      _serviceConfigState.tone = 'error';
      renderServiceConfigModal();
    }
  });
}

// ── Data fetching ───────────────────────────────────────────────

async function fetchFast(client) {
  try {
    const [metrics, eventsData, requestsData] = await Promise.all([
      client.call('system.metrics'),
      client.call('system.events', { limit: 50 }),
      client.call('system.activeRequests'),
    ]);
    return { metrics, eventsData, requestsData };
  } catch {
    return null;
  }
}

async function fetchSlow(client) {
  const [
    health,
    services,
    localBackends,
    dockerDependencies,
    observabilitySources,
    observabilitySeries,
    sessionAnalytics,
    contextUsage,
    config,
    modelCatalog,
  ] = await Promise.allSettled([
    client.call('system.health'),
    client.call('system.services'),
    client.call('system.localBackends'),
    client.call('system.dockerDependencies'),
    client.call('system.observabilitySources'),
    client.call('system.observabilitySeries', { windowMinutes: 60, bucketSeconds: 30 }),
    client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
    client.call('system.contextUsage'),
    client.call('config.get'),
    client.call('system.modelCatalog'),
  ]);

  const unwrap = (result) => (result.status === 'fulfilled' ? result.value : null);

  const configValue = unwrap(config);
  const modelCatalogValue = unwrap(modelCatalog);
  if (configValue && typeof configValue === 'object') {
    configValue.__modelCatalog = Array.isArray(modelCatalogValue?.providers) ? modelCatalogValue.providers : [];
  }

  return {
    health: unwrap(health),
    services: unwrap(services),
    localBackends: unwrap(localBackends),
    dockerDependencies: unwrap(dockerDependencies),
    observabilitySources: unwrap(observabilitySources),
    observabilitySeries: unwrap(observabilitySeries),
    sessionAnalytics: unwrap(sessionAnalytics),
    contextUsage: unwrap(contextUsage),
    config: configValue,
  };
}

// ── Main load function ──────────────────────────────────────────

let _lastHealth = null;
let _lastMetrics = null;

async function loadDashboard(el, client) {
  _dashboardClient = client;
  renderSkeleton(el);

  // Fetch everything initially
  const [fast, slow] = await Promise.all([
    fetchFast(client),
    fetchSlow(client),
  ]);

  _lastHealth = slow?.health ?? null;
  _lastMetrics = fast?.metrics ?? null;

  if (fast) {
    updateCounters(fast.metrics, _lastHealth);
    updateModelTable(fast.metrics);
    updateEvents(fast.eventsData);
    updateActiveRequests(fast.requestsData);
  }
  if (slow?.config) {
    _lastAssistantConfig = slow.config;
  }
  if (slow?.services) {
    updateServices(slow.services);
  }
  if (slow?.localBackends) {
    updateLocalBackends(slow.localBackends);
  }
  if (slow?.dockerDependencies) {
    updateDockerDependencies(slow.dockerDependencies);
  }
  if (slow?.observabilitySources) {
    _lastObservabilitySources = Array.isArray(slow.observabilitySources.sources)
      ? slow.observabilitySources.sources
      : [];
  }
  if (slow?.observabilitySeries) {
    updateObservabilityGraphs(slow.observabilitySeries);
  } else {
    updateObservabilityGraphs({ series: [] });
  }
  updateServiceLogsPanel();
  await refreshServiceLogs({ force: true });
  if (slow?.sessionAnalytics) {
    updateSessionAnalytics(slow.sessionAnalytics);
  }
  if (slow?.contextUsage) {
    updateContextHealth(slow.contextUsage);
  }
  if (slow?.config) {
    updateAssistantHealth(_lastAssistantConfig);
  }

  // Fast refresh: 3 seconds for metrics, events, requests
  _fastTimer = setInterval(async () => {
    const data = await fetchFast(client);
    if (data) {
      _lastMetrics = data.metrics;
      updateCounters(data.metrics, _lastHealth);
      updateModelTable(data.metrics);
      updateEvents(data.eventsData);
      updateActiveRequests(data.requestsData);
    }
  }, 3000);

  // Slow refresh: 10 seconds for health, services
  _slowTimer = setInterval(async () => {
    const data = await fetchSlow(client);
    if (data.health) {
      _lastHealth = data.health;
      updateCounters(_lastMetrics, data.health);
    }
    if (data.config) {
      _lastAssistantConfig = data.config;
    }
    if (data.services) {
      updateServices(data.services);
    }
    if (data.localBackends) {
      updateLocalBackends(data.localBackends);
    }
    if (data.dockerDependencies) {
      updateDockerDependencies(data.dockerDependencies);
    }
    if (data.observabilitySources) {
      _lastObservabilitySources = Array.isArray(data.observabilitySources.sources)
        ? data.observabilitySources.sources
        : [];
      updateServiceLogsPanel();
    }
    if (data.observabilitySeries) {
      updateObservabilityGraphs(data.observabilitySeries);
    }
    if (data.sessionAnalytics) {
      updateSessionAnalytics(data.sessionAnalytics);
    }
    if (data.contextUsage) {
      updateContextHealth(data.contextUsage);
    }
    if (data.config) {
      updateAssistantHealth(_lastAssistantConfig);
    }
  }, 10000);

  _logRefreshTimer = setInterval(async () => {
    await refreshServiceLogs();
  }, 5000);
}

export const DashboardPage = {
  async render(el, client) {
    await loadDashboard(el, client);
  },

  teardown() {
    if (_fastTimer) {
      clearInterval(_fastTimer);
      _fastTimer = null;
    }
    if (_slowTimer) {
      clearInterval(_slowTimer);
      _slowTimer = null;
    }
    if (_logRefreshTimer) {
      clearInterval(_logRefreshTimer);
      _logRefreshTimer = null;
    }
    _lastHealth = null;
    _lastMetrics = null;
    _dashboardClient = null;
    _lastPlaybookRollbackPatches = null;
    _lastBriefingTestAt = null;
    _assistantSaveState = null;
    _lastAssistantConfig = null;
    _assistantManualOverrides = new Set();
    _assistantModelDefaultsDraft = null;
    _assistantDraftState = new Map();
    _assistantDraftTouchedAt = 0;
    _lastServices = [];
    _lastLocalBackends = [];
    _lastDockerDependencies = [];
    _lastObservabilitySources = [];
    _lastObservabilitySeries = null;
    _lastServiceLogs = null;
    _selectedLogSourceId = null;
    _logViewerState = {
      lines: 200,
      sinceSeconds: 900,
      level: 'all',
      autoRefresh: true,
      paused: false,
      status: null,
      tone: 'neutral',
    };
    _localBackendActionState = new Map();
    _dockerDependencyActionState = new Map();
    _serviceConfigState = {
      open: false,
      serviceName: null,
      status: null,
      tone: 'neutral',
      advancedPatch: '',
    };
    _lastCouncilTask = '';
    _lastCouncilResult = null;
    _lastCouncilError = null;
  },
};
