// monitor.js
// Scope: display and active state — what is seen, what is happening.
// Requires mgrSNP.js loaded first (window.SNP).
// Input surfaces: chat input field + transport buttons only.
// Everything else is poll → receive → display.

// ── CONFIG ────────────────────────────────────────────────────────────────────

let DECK_URL = DeckConnection.getURL();

// ── ACTIVE STATE (5 system booleans) ─────────────────────────────────────────
// These drive the LED indicators and monitor message display.

let recorderRunning    = false;  // solid orange
let aiEngaged          = false;  // solid orange (overrides recorder message)
let yesItemsReady      = false;  // flashing orange — approved items waiting to run
let proposalsAwaiting  = false;  // solid green — tape has proposals, none approved yet
let recentChatManifest = null;   // flashing green — 20s quick-accept window
let recentChatTimer    = null;
let recentChatBlink    = null;
let runnableDubFlashUntil = 0;    // flashing green — 10s after runnable diagnostic/enhancement arrives
let runnableDubBlink      = null;
let lastRunnableDubKey    = '';

// ── UI STATE ──────────────────────────────────────────────────────────────────

let pollTimer              = null;
let pollBusy               = false;
let pollCycle              = 0;
let logLines               = [];
let systemLogSeeded        = false;
let lastSidebarLine        = '';
let lastSidebarActivityAt  = Date.now();
let systemLogGroupCounter  = 0;
let currentSidebarGroup    = 'system-group-0';
let dubFilters             = new Set(['error', 'diagnostic', 'enhancement', 'run']);
let latestDubTape          = [];
let latestDubViewItems     = [];
let expandedDubKey         = null;
let selectedDubIndex       = -1;
let selectedDubManifest    = null;
let selectedDubKind        = 'diagnostic';
let selectedDubSource      = 'monitor';
let selectedTapeIndex      = -1;
let selectedSystemGroupIdx = -1;
let selectedProjectUnitIndex = -1;
let currentSidebarGroupTime = 0;
let sidebarRunMeta = new Map();
const ACTIVITY_CLUSTER_MS = 3 * 60 * 1000;
let connectedPortRevealTimer = null;
const HISTORY_WINDOW_STEPS = [
  { label: '48 hours', ms: 48 * 60 * 60 * 1000 },
  { label: '1 week', ms: 7 * 24 * 60 * 60 * 1000 },
  { label: '2 weeks', ms: 14 * 24 * 60 * 60 * 1000 }
];
let historyWindowStep = 0;
let historyExtraMonths = 0;


let agreementGateActive = false;
let agreementPolicyVersion = 'beta-agreement-v1';
let projectSettings = [];
let pendingActivationId = '';
let pendingActivationUrl = '';
let activationPollTimer = null;
const ACCOUNT_PORTAL_URL = 'https://nodarama.com/account/';

// ── HELPERS ───────────────────────────────────────────────────────────────────

function byId(id) { return document.getElementById(id); }

function on(id, event, fn) {
  const el = byId(id);
  if (el) el.addEventListener(event, fn);
}

function toast(msg, dur = 2500) {
  const el = byId('toast');
  if (!el) return;
  el.textContent = msg;
  el.classList.add('show');
  clearTimeout(el._t);
  el._t = setTimeout(() => el.classList.remove('show'), dur);
}

function esc(s) {
  return String(s ?? '')
    .replace(/&/g, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function downloadText(text, filename) {
  const a = Object.assign(document.createElement('a'), {
    href: URL.createObjectURL(new Blob([text], { type: 'text/plain' })),
    download: filename
  });
  a.click();
  URL.revokeObjectURL(a.href);
}

async function api(path, method = 'GET', body = null) {
  const verb = String(method || 'GET').toUpperCase();
  const route = String(path || '');
  const isPollingRead = verb === 'GET' && /^(\/status|\/logs\/|\/dubs\/playback|\/playback|\/projects|\/auth\/status|\/policy\/combined)/.test(route);
  const isExplicitFileRead = /^\/read\//.test(route);

  if (/^\/record/.test(route)) triggerEQActivity('record');
  else if (/^\/eq\/chat/.test(route)) triggerEQActivity('chat');
  else if (/^\/playback\/approve/.test(route)) triggerEQActivity('approve');
  else if (isExplicitFileRead) triggerEQActivity('readfile');
  else if (verb !== 'GET' && !isPollingRead) triggerEQActivity('medium');

  try {
    const opts = { method, headers: {} };
    if (body !== null) {
      opts.headers['Content-Type'] = 'application/json';
      opts.body = JSON.stringify(body);
    }
    const res = await fetch(DECK_URL + path, opts);
    return await res.json();
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

function formatAuthFailure(result, fallback = 'Auth failed') {
  const connected = result?.connected === true ? 'connected' : result?.connected === false ? 'not connected' : '';
  const status = result?.remoteStatus || result?.status || '';
  const type = String(result?.failureType || '').replace(/_/g, ' ');
  const detail = result?.message || result?.error || fallback;
  return [detail, type, status ? String(status) : '', connected].filter(Boolean).join(' · ');
}

function resolveDefaultTabFromAuth(auth) {
  const hasUser = Boolean(auth?.userId || auth?.email);
  const isSignedIn = auth?.status === "signed_in" || (hasUser && auth?.tier && auth.tier !== "limited");

  return isSignedIn ? "project" : "settings";
}

function activateTab(tabName) {
  document.querySelectorAll('.tab-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.tab === tabName);
  });

  document.querySelectorAll('.tab-panel').forEach(panel => {
    panel.classList.toggle('active', panel.id === `panel-${tabName}`);
  });

  if (tabName === 'dubs') refreshDubTape();
  if (tabName === 'history') refreshTape();
}

function setAccountView(auth) {
  const statusEl = byId('account-status');
  const tierEl = byId('account-tier');
  const emailEl = byId('set-auth-email');
  const passwordEl = byId('set-auth-password');

  const signedIn = auth?.status === 'signed_in' || Boolean(auth?.userId || auth?.email);
  const tier = auth?.tier || 'limited';
  const email = auth?.email || '';

  if (statusEl) statusEl.textContent = signedIn ? 'Signed in' : 'Signed out';
  if (tierEl) tierEl.textContent = tier;
  if (emailEl && !emailEl.value) emailEl.value = email;
  if (signedIn && passwordEl) passwordEl.value = '';
}

function setAgreementGateVisible(visible) {
  agreementGateActive = Boolean(visible);
  const modal = byId('agreement-gate-modal');
  if (modal) modal.style.display = visible ? 'flex' : 'none';
}

function isSignedInAuth(auth) {
  return Boolean(auth?.status === 'signed_in' || auth?.userId || auth?.email);
}

async function loadAgreementPolicyText() {
  const box = byId('agreement-policy-text');
  const data = await api('/policy/combined');
  if (data?.ok) {
    agreementPolicyVersion = data.version || 'beta-agreement-v1';
    if (box) box.textContent = data.text || 'No policy text found.';
  } else if (box) {
    box.textContent = 'Unable to load agreement text. Restart or check policy files.';
  }
}

async function enforceAgreementGate(settings, auth) {
  const accepted = Boolean(settings?.agreement?.accepted && settings?.agreement?.acceptedAt);
  const signedIn = isSignedInAuth(auth);
  if (accepted && signedIn) {
    setAgreementGateVisible(false);
    return false;
  }
  await loadAgreementPolicyText();
  const email = byId('agreement-email');
  if (email && !email.value) email.value = byId('set-auth-email')?.value || settings?.authEmail || '';
  setAgreementGateVisible(true);
  activateTab('settings');
  return true;
}

async function saveAgreementAccepted(authResult, fallbackEmail) {
  await SettingsStore.save({
    agreement: { version: agreementPolicyVersion, acceptedAt: Date.now(), accepted: true },
    authEmail: authResult?.user?.email || fallbackEmail || '',
    authUserId: authResult?.user?.userId || authResult?.user?.id || '',
    authTier: authResult?.user?.tier || authResult?.tier || 'limited',
    authStatus: 'signed_in'
  });
}

async function agreementGateLogin(createAccount = false) {
  const accepted = Boolean(byId('agreement-accept-check')?.checked);
  const email = byId('agreement-email')?.value.trim() || '';
  const password = byId('agreement-password')?.value || '';
  if (!accepted) { toast('Accept the agreement first.'); return; }
  if (!email || !password) { toast('Enter email and password.'); return; }

  const endpoint = createAccount ? '/auth/register' : '/auth/login';
  const payload = createAccount
    ? { email, password, deviceName: navigator.platform || 'NV Station', deviceFingerprint: navigator.userAgent || 'unknown' }
    : { email, password };

  const result = await api(endpoint, 'POST', payload);

  if (!result?.ok) {
    const message = formatAuthFailure(result, createAccount ? 'Create account failed' : 'Login failed');
    toast(message, 7000);
    appendLog(`✗ agreement gate ${createAccount ? 'create account' : 'login'} failed: ${message}`, 'fail');
    return;
  }

  if (result.activationRequired) {
    toast(result.message || 'Activation required. Complete activation, then log in.', 9000);
    if (result.activationUrl) window.open(result.activationUrl, '_blank', 'noopener');
    return;
  }

  await saveAgreementAccepted(result, email);
  setAgreementGateVisible(false);

  const auth = await refreshAccountUI();
  await loadSettingsUI();
  await loadProjectSelector();

  if (window.LaunchState) {
    LaunchState.auth = auth;
    LaunchState.settings = await SettingsStore.load();
    LaunchState.backstage = isBackstageAuth(LaunchState.auth);
    LaunchState.projects = await fetchLaunchProjects();
    await renderLaunchState();
  } else if (typeof bootLaunchGate === 'function') {
    await bootLaunchGate();
  } else {
    activateTab('project');
  }

  toast('Agreement accepted. Account active.');
}


async function refreshAccountUI() {
  const status = await api('/auth/status');

  if (status?.ok) {
    const signedIn = Boolean(status.authenticated || status.email);
    const authState = {
      userId: status.userId || '',
      email: status.email || '',
      tier: status.accessTier || status.tier || 'limited',
      status: signedIn ? 'signed_in' : 'signed_out'
    };

    setAccountView(authState);

    await SettingsStore.save({
      authEmail: authState.email,
      authUserId: authState.userId,
      authTier: authState.tier,
      authStatus: authState.status
    });

    return authState;
  }

  const settings = await SettingsStore.load();
  if (!settings) return null;

  const fallback = {
    userId: settings.authUserId,
    email: settings.authEmail,
    tier: settings.authTier,
    status: settings.authStatus
  };
  setAccountView(fallback);
  return fallback;
}
function isRunnableDubItem(item) {
  return Boolean(item && item.status === 'proposal' && item.manifest);
}

function getRunnableDubKey(item) {
  if (!item) return '';
  return `${item.type || ''}:${item.dubID || ''}:${item.action || ''}:${item.filePath || ''}`;
}

function hasRunnableDubSelected() {
  const item = latestDubViewItems[selectedDubIndex] || null;
  return Boolean(selectedDubManifest && isRunnableDubItem(item));
}

function hasRunnableDubFlashWindow() {
  return Date.now() < runnableDubFlashUntil;
}

function getSelectedRunnableDubItem() {
  const item = latestDubViewItems[selectedDubIndex] || null;
  return isRunnableDubItem(item) ? item : null;
}

function getLatestRunnableDubItem() {
  return latestDubViewItems.find(isRunnableDubItem) || buildDubItems(latestDubTape).find(isRunnableDubItem) || null;
}

async function approveDubItem(item, sourceLabel = 'monitor') {
  if (!item?.manifest) return null;
  const kind = item.type === 'enhancement' ? 'enhancement' : item.type === 'error' ? 'error' : 'diagnostic';
  const data = await api('/playback/approve', 'POST', {
    manifest: item.manifest,
    source: item.source || sourceLabel,
    kind
  });
  if (data?.ok) {
    showRecorderUsageNotice(data);
    appendLog(`▶ ${sourceLabel} manifest approved${data.dubID ? ` → dubID:${data.dubID}` : ''}`, 'pass');
    selectedDubManifest = null;
    await refreshStatus();
    await refreshDubTape();
    await refreshTape();
  } else {
    showRecorderUsageNotice(data);
    toast('Approve failed: ' + (data?.message || data?.error || 'unknown'));
    appendLog(`✗ approve failed: ${data?.message || data?.error || 'unknown'}`, 'fail');
  }
  return data;
}

function updateIndicators() {
  const led    = byId('play-led');
  const recLed = byId('record-led');
  const monMsg = byId('monitor-message');
  const playBtn = byId('btn-play');
  const recBtn  = byId('btn-record');

  if (recentChatBlink) {
    clearInterval(recentChatBlink);
    recentChatBlink = null;
  }
  if (runnableDubBlink) {
    clearInterval(runnableDubBlink);
    runnableDubBlink = null;
  }
  if (recLed?._blink) {
    clearInterval(recLed._blink);
    recLed._blink = null;
  }

  const selectedRunnable = getSelectedRunnableDubItem();
  const latestRunnable = getLatestRunnableDubItem();
  const hasManualQuickAccept = Boolean(recentChatManifest || hasRunnableDubFlashWindow());
  const hasWaitingManifest = Boolean(proposalsAwaiting || latestRunnable);

  if (led) led.classList.remove('active');
  if (recLed) recLed.classList.remove('active');

  if (aiEngaged) {
    if (recLed) recLed.classList.add('active');
    if (monMsg) monMsg.textContent = 'LOCAL AI WORKING';
    if (recBtn) recBtn.title = 'RECORD — local AI is working';
    return;
  }

  if (hasManualQuickAccept) {
    let on = false;
    runnableDubBlink = setInterval(() => {
      on = !on;
      if (led) led.classList.toggle('active', on);
      if (!recentChatManifest && !hasRunnableDubFlashWindow()) {
        clearInterval(runnableDubBlink);
        runnableDubBlink = null;
        updateIndicators();
      }
    }, 260);
    if (monMsg) monMsg.textContent = 'NEW MANIFEST — PRESS PLAY';
    if (playBtn) playBtn.title = 'PLAY — quick accept newest manifest';
    return;
  }

  if (yesItemsReady) {
    if (led) led.classList.add('active');
    if (monMsg) monMsg.textContent = 'AUTO ITEMS READY — PRESS RECORD';
    if (playBtn) playBtn.title = 'PLAY — auto items are ready';
    if (recBtn) recBtn.title = 'RECORD — process approved auto items';
    return;
  }

  if (selectedRunnable) {
    if (led) led.classList.add('active');
    if (recLed) recLed.classList.add('active');
    if (monMsg) monMsg.textContent = 'MANIFEST SELECTED — PLAY OR RECORD';
    if (playBtn) playBtn.title = 'PLAY — process selected manifest';
    if (recBtn) recBtn.title = 'RECORD — process selected manifest';
    return;
  }

  if (hasWaitingManifest) {
    if (recLed && !recLed._blink) {
      let on = false;
      recLed._blink = setInterval(() => { on = !on; recLed.classList.toggle('active', on); }, 420);
    }
    if (monMsg) monMsg.textContent = 'MANIFEST WAITING';
    if (recBtn) recBtn.title = 'RECORD — manifest waiting';
    return;
  }

  if (recorderRunning) {
    if (recLed) recLed.classList.add('active');
    if (monMsg) monMsg.textContent = 'RECORDING IN PROGRESS';
    if (recBtn) recBtn.title = 'Recorder is running';
    return;
  }

  if (monMsg) monMsg.textContent = '';
  if (playBtn) playBtn.title = 'PLAY — wake recorder';
  if (recBtn) recBtn.title = 'RECORD — run approved items';
}

function setTemporaryMessage(text, durationMs = 4000) {
  const monMsg = byId('monitor-message');
  if (!monMsg) return;
  monMsg.textContent = text;
  clearTimeout(monMsg._t);
  monMsg._t = setTimeout(() => updateIndicators(), durationMs);
}

function getConnectedPortLabel() {
  try {
    const url = new URL(DeckConnection.getURL());
    return url.port || (url.protocol === 'https:' ? '443' : url.protocol === 'http:' ? '80' : 'unknown');
  } catch {
    return 'unknown';
  }
}

function setConnectedPortMasked() {
  const el = byId('connected-port-display');
  if (el) el.textContent = 'port: *****';
}

function revealConnectedPortTemporarily() {
  const el = byId('connected-port-display');
  if (!el) return;
  el.textContent = `port: ${getConnectedPortLabel()}`;
  clearTimeout(connectedPortRevealTimer);
  connectedPortRevealTimer = setTimeout(setConnectedPortMasked, 100);
}



// ── TABS ──────────────────────────────────────────────────────────────────────

document.querySelectorAll('.tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    activateTab(btn.dataset.tab);
  });
});

document.querySelectorAll('.htab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.htab-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.hpanel').forEach(p => p.classList.remove('active'));
    btn.classList.add('active');
    const panel = byId('hpanel-' + btn.dataset.htab);
    if (panel) panel.classList.add('active');
    loadLogStream(btn.dataset.htab);
  });
});

function showRecorderUsageNotice(payload) {
  const notice = payload?.notice || null;
  const usage = payload?.usage || null;
  const code = notice?.code || payload?.error || usage?.code || '';
  const message = notice?.message || payload?.message || usage?.message || '';

  if (!message && !code) return;

  if (code === 'limited_bonus_runs_granted' || code === 'limited_bonus_run_available') {
    toast(message || 'Bonus day: you have 5 extra RECORD runs today. Launch sooner — keep going.', 9000);
    appendLog('★ bonus RECORD runs active: ' + (message || '5 extra runs today'), 'pass');
    return;
  }

  if (code === 'limited_daily_record_warning') {
    toast(message || 'You used your 5 RECORD runs today. Upgrade for unlimited runs and launch sooner.', 8000);
    appendLog('ⓘ limited RECORD runs used: ' + (message || '5/5 today'), 'neutral');
    return;
  }

  if (code === 'limited_daily_record_limit_reached' || code === 'limited_bonus_runs_exhausted') {
    toast(message || 'Daily RECORD limit reached. Upgrade to Standard for unlimited runs.', 100);
    appendLog('✗ RECORD limit reached: ' + (message || code), 'fail');
  }
}

function requireAgreementForAction() {
  if (!agreementGateActive) return true;
  toast('Accept the agreement and log in first.');
  activateTab('settings');
  return false;
}

// ── TRANSPORT BUTTONS ─────────────────────────────────────────────────────────

on('send-manifest-btn', 'click', () => { void sendInput(); });
on('chat-input', 'keydown', event => {
  if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
    event.preventDefault();
    void sendInput();
  }
});
on('run-now-btn', 'click', async () => {
  const input = byId('manifest-input');
  const raw = input?.value.trim() || '';
  if (!raw) { toast('Enter manifest JSON'); return; }
  try {
    const parsed = JSON.parse(raw);
    const data = await api('/record', 'POST', parsed);
    if (data.ok) {
      showRecorderUsageNotice(data);
      if (input) input.value = '';
      appendLog(`⏺ sent manifest${data.dubID ? ` → dubID:${data.dubID}` : ''}`, 'pass');
      await refreshStatus();
      await refreshDubTape();
      await refreshTape();
    } else {
      showRecorderUsageNotice(data);
      toast('Error: ' + (data.message || data.error || 'unknown'));
      appendLog(`✗ manifest failed: ${data.message || data.error || 'unknown'}`, 'fail');
    }
  } catch (err) {
    toast('JSON syntax error — ' + String(err?.message || err).slice(0, 80));
    appendLog(`✗ JSON parse error: ${err?.message || err}`, 'fail');
  }
});

on('btn-play', 'click', async () => {
  if (!requireAgreementForAction()) return;

  const quickManifestItem = recentChatManifest?.manifest
    ? {
        manifest: recentChatManifest.manifest,
        type: recentChatManifest.kind || 'diagnostic',
        source: recentChatManifest.source || 'local_ai'
      }
    : hasRunnableDubFlashWindow()
      ? getLatestRunnableDubItem()
      : null;

  if (quickManifestItem?.manifest) {
    clearChatManifestWindow();
    await approveDubItem(quickManifestItem, 'quick accept');
    return;
  }

  const selectedItem = getSelectedRunnableDubItem();
  if (selectedItem) {
    await approveDubItem(selectedItem, 'selected');
    return;
  }

  const data = await api('/record/check', 'POST');
  appendLog(`▶ recorder check → ${data.ok ? data.state || 'ok' : data.error}`, data.ok ? 'pass' : 'fail');
  refreshStatus();
});

on('btn-record', 'click', async () => {
  if (!requireAgreementForAction()) return;

  const selectedItem = getSelectedRunnableDubItem();
  if (selectedItem) {
    await approveDubItem(selectedItem, 'selected');
    const run = await api('/record/check', 'POST');
    appendLog(`⏺ record selected → ${run.ok ? run.state || 'ok' : run.error}`, run.ok ? 'pass' : 'fail');
    await refreshStatus();
    await refreshDubTape();
    return;
  }

  const data = await api('/record/check', 'POST');
  appendLog(`⏺ record → ${data.ok ? data.state || 'ok' : data.error}`, data.ok ? 'pass' : 'fail');
  await refreshStatus();
  await refreshDubTape();
});

on('btn-pause', 'click', async () => {
  if (!requireAgreementForAction()) return;
  if (recorderRunning) {
    const data = await api('/pause', 'POST');
    if (data.ok) { appendLog('⏸ recorder paused', 'neutral'); await refreshStatus(); }
    return;
  }
  if (await SNP.isAutoAcceptEnabled()) {
    await SettingsStore.save({ autoPlayHandling: { chatManifests: false, diagnosticRepair: false, enhancementSuggestions: false } });
    appendLog('⏸ auto accept suspended', 'neutral');
    toast('Auto accept suspended');
    await loadSettingsUI();
    return;
  }
  toast('Nothing to pause');
});

on('btn-prev',  'click', () => handleNavigation(-1));
on('btn-next',  'click', () => handleNavigation(1));
on('btn-eject', 'click', () => { if (window.SysMenu) window.SysMenu.toggle(); });
on('connected-port-display', 'click', revealConnectedPortTemporarily);
on('qc-refresh', 'click', refreshAll);
on('qc-status',  'click', refreshStatus);

// ── NAVIGATION ────────────────────────────────────────────────────────────────

function parseTimestampMs(line) {
  const match = String(line || '').match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/);
  if (!match) return 0;
  const dt = new Date(`${match[1]}T${match[2]}`);
  return Number.isFinite(dt.getTime()) ? dt.getTime() : 0;
}

function getLogActivityClusters(items) {
  const clusters = [];
  let current = [];
  let lastTime = 0;

  for (const item of items || []) {
    const time = parseTimestampMs(item.textContent || '');
    if (current.length && time && lastTime && time - lastTime > ACTIVITY_CLUSTER_MS) {
      clusters.push(current);
      current = [];
    }
    current.push(item);
    if (time) lastTime = time;
  }

  if (current.length) clusters.push(current);
  return clusters;
}

function selectCluster(clusters, index) {
  clusters.flat().forEach(el => el.classList.remove('selected'));
  const cluster = clusters[index] || [];
  cluster.forEach(el => el.classList.add('selected'));
  cluster[0]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}

function handleNavigation(direction) {
  const tab = document.querySelector('.tab-btn.active')?.dataset.tab || 'project';
  if (tab === 'editor')  navigateEditor(direction);
  else if (tab === 'dubs')    navigateDubs(direction);
  else if (tab === 'history') navigateHistory(direction);
  else if (tab === 'project') navigateProject(direction);
  else navigateLogGroups(direction);
}

function clamp(index, length, direction) {
  if (!length) return -1;
  if (index < 0) return direction > 0 ? 0 : length - 1;
  const next = index + direction;
  if (next < 0) return length - 1;
  if (next >= length) return 0;
  return next;
}

function selectElement(items, index) {
  items.forEach((el, i) => el.classList.toggle('selected', i === index));
  items[index]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}

function navigateDubs(direction) {
  const items = [...document.querySelectorAll('#dubs-list .dub-item')].filter(el => !el.classList.contains('run'));
  if (!items.length) return navigateLogGroups(direction);
  selectedDubIndex = clamp(selectedDubIndex, items.length, direction);
  selectElement(items, selectedDubIndex);
  items[selectedDubIndex]?.classList.add('expanded');
  syncSelectedManifest();
}

function navigateHistory(direction) {
  const activePanel = document.querySelector('#panel-history .hpanel.active');
  const isTape = activePanel?.id === 'hpanel-tape';
  const items = isTape
    ? [...document.querySelectorAll('#tape-list .tape-item')]
    : [...(activePanel?.querySelectorAll('.log-stream-line') || [])];

  if (!items.length) return navigateLogGroups(direction);

  if (isTape) {
    selectedTapeIndex = clamp(selectedTapeIndex, items.length, direction);
    selectElement(items, selectedTapeIndex);
    items[selectedTapeIndex]?.classList.add('expanded');
    return;
  }

  const clusters = getLogActivityClusters(items);
  if (!clusters.length) return navigateLogGroups(direction);
  selectedTapeIndex = clamp(selectedTapeIndex, clusters.length, direction);
  selectCluster(clusters, selectedTapeIndex);
}

function navigateEditor(direction) {
  const textarea = byId('editor-textarea');
  if (!textarea) return navigateLogGroups(direction);
  const lines = textarea.value.split('\n');
  const ranges = [];
  let offset = 0;
  const patterns = [
    /^\s*function\s+\w+\s*\(/,
    /^\s*(const|let|var)\s+\w+\s*=\s*(async\s*)?\([^)]*\)\s*=>/,
    /^\s*(const|let|var)\s+\w+\s*=\s*(async\s*)?function\s*\(/,
    /^\s*(async\s+)?\w+\s*\([^)]*\)\s*\{/,
    /^\s*class\s+\w+/
  ];
  for (let i = 0; i < lines.length; i++) {
    if (patterns.some(p => p.test(lines[i]))) ranges.push({ start: offset, end: offset + lines[i].length });
    offset += lines[i].length + 1;
  }
  if (!ranges.length) return navigateLogGroups(direction);
  const pos = textarea.selectionStart || 0;
  let cur = ranges.findIndex((r, idx) => pos >= r.start && pos < (ranges[idx + 1]?.start ?? Infinity));
  if (cur < 0) cur = direction > 0 ? -1 : 0;
  const target = ranges[clamp(cur, ranges.length, direction)];
  textarea.focus();
  textarea.setSelectionRange(target.start, target.end);
  textarea.scrollTop = Math.max(0, (textarea.value.slice(0, target.start).split('\n').length - 2) * 20);
}

function navigateLogGroups(direction) {
  const rows = [...document.querySelectorAll('#system-log .log-line')];
  const groups = [];
  let cur = [], curGroup = null;
  for (const row of rows) {
    const g = row.dataset.runGroup || 'system-group-0';
    if (g !== curGroup) { if (cur.length) groups.push(cur); cur = [row]; curGroup = g; }
    else cur.push(row);
  }
  if (cur.length) groups.push(cur);
  if (!groups.length) return;
  selectedSystemGroupIdx = clamp(selectedSystemGroupIdx, groups.length, direction);
  groups.forEach((g, gi) => g.forEach(r => r.classList.toggle('selected', gi === selectedSystemGroupIdx)));
  groups[selectedSystemGroupIdx]?.[0]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}

function extractAIResponseText(value) {
  const raw = String(value ?? '').trim();
  if (!raw) return '';
  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return String(parsed.Response || parsed.response || parsed.summary || '').trim() || raw;
    }
  } catch {}
  const match = raw.match(/"Response"\s*:\s*"((?:\\"|[^"])*)"/i);
  if (match?.[1]) {
    return match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
  }
  return raw;
}

async function sendInput() {
  if (agreementGateActive) {
    toast('Accept the agreement and log in first.');
    activateTab('settings');
    return;
  }

  const input = byId('chat-input');
  const raw = input?.value.trim() || '';
  if (!raw) return;

  // valid JSON → /record
  if (raw.startsWith('{') || raw.startsWith('[')) {
    let parsed;
    try { parsed = JSON.parse(raw); } catch (e) {
      toast('JSON syntax error — ' + String(e.message).slice(0, 80));
      appendLog(`✗ JSON parse error: ${e.message}`, 'fail');
      return;
    }
    const manifests = Array.isArray(parsed) ? parsed : [parsed];
    const data = await api('/record', 'POST', parsed);
    if (data.ok) {
      showRecorderUsageNotice(data);
      appendLog(
        manifests.length > 1
          ? `⏺ sent ${data.accepted || manifests.length} manifest${(data.accepted || manifests.length) !== 1 ? 's' : ''}`
          : `⏺ inject → dubID:${data.dubID || '?'} ${data.status || 'running'}`,
        'pass'
      );
      if (input) input.value = '';
    } else {
      showRecorderUsageNotice(data);
      appendLog(`✗ inject failed: ${data.message || data.error}`, 'fail');
      toast('Error: ' + (data.message || data.error));
    }
    await refreshStatus();
    refreshDubTape();
    return;
  }

  // everything else → /eq/chat
  appendLog(`CHAT OUT → ${raw.slice(0, 120)}${raw.length > 120 ? '…' : ''}`, 'chat-out');
  if (input) input.value = '';
  aiEngaged = true;
  updateIndicators();
  try {
    const data = await api('/eq/chat', 'POST', { message: raw });
    if (!data.ok) {
      appendLog(`✗ chat failed: ${data.error || 'unknown'}`, 'fail');
      toast('Chat error: ' + (data.error || 'unknown'));
      return;
    }
    if (data.reply) {
      const reply = extractAIResponseText(data.reply);
      appendLog(`CHAT IN ← ${reply}`, 'chat-in');
    }
  } finally {
    aiEngaged = false;
    clearEQSustain('chat');
    triggerEQActivity('low');
    updateIndicators();
  }
  // any manifest EQ generates lands on tape — refreshDubTape polling picks it up
}

async function readFileAction() {
  const raw = byId('chat-input')?.value.trim() || '';
  if (!raw) { toast('Enter a file path'); return; }
  const data = await api('/read/file', 'POST', { path: raw });
  if (data.ok && data.files?.[0]) {
    const f = data.files[0];
    if (f.ok) { const input = byId('chat-input'); if (input) input.value = (f.content || '').slice(0, 2000); appendLog(`📄 read ${f.FilePath}`, 'pass'); }
    else appendLog(`✗ read failed: ${f.error}`, 'fail');
  }
}

// ── CHAT MANIFEST WINDOW ──────────────────────────────────────────────────────

function openChatManifestWindow(entry) {
  clearChatManifestWindow();
  if (!entry?.manifest) return;
  recentChatManifest = entry;
  recentChatTimer = setTimeout(() => clearChatManifestWindow(), 20000);
  updateIndicators();
}

function clearChatManifestWindow() {
  if (recentChatTimer) clearTimeout(recentChatTimer);
  if (recentChatBlink) clearInterval(recentChatBlink);
  recentChatTimer = null;
  recentChatBlink = null;
  recentChatManifest = null;
  updateIndicators();
}

// ── DUB TAPE ──────────────────────────────────────────────────────────────────

function syncSelectedManifest() {
  const item = latestDubViewItems[selectedDubIndex] || null;
  if (!item?.manifest || item.status !== 'proposal' || !isRunnableDubItem(item)) {
    selectedDubManifest = null;
    selectedDubKind = 'diagnostic';
    selectedDubSource = 'monitor';
  } else {
    selectedDubManifest = item.manifest;
    selectedDubKind = item.type === 'enhancement' ? 'enhancement' : item.type === 'error' ? 'error' : 'diagnostic';
    selectedDubSource = item.source || 'monitor';
  }
  updateIndicators();
}

// legacy Monitor project splash removed; project switch splash is owned by DeckProject.initiateProjectSwitch.
async function refreshDubTape() {
  const data = await api('/dubs/playback');
  if (!data.ok) return;

  latestDubTape = data.tape || [];
  const latestCompletedDub = latestDubTape.slice().reverse().find(item => item?.status === 'done' || item?.status === 'error') || null;
  const latestCompletedKey = latestCompletedDub
    ? `${latestCompletedDub.status || ''}:${latestCompletedDub.dubID || ''}:${latestCompletedDub.timestamp || ''}`
    : '';
  if (!lastCompletedDubKey && latestCompletedKey) {
    lastCompletedDubKey = latestCompletedKey;
  } else if (latestCompletedKey && latestCompletedKey !== lastCompletedDubKey) {
    lastCompletedDubKey = latestCompletedKey;
    clearEQSustain();
    if (latestCompletedDub.status === 'done') {
      triggerEQActivity('medium');
      setTemporaryMessage('✓ RUN COMPLETE', 5000);
      appendLog('✓ run complete', 'pass');
    } else {
      triggerEQActivity('high');
      setTemporaryMessage('✗ RUN FAILED', 6000);
      appendLog('✗ run failed', 'fail');
    }
  }

  const proposals = latestDubTape.filter(i => i.status === 'proposal');
  yesItemsReady    = proposals.some(i => i.recorderStatus === 'YES');
  proposalsAwaiting = proposals.some(i => i.recorderStatus === 'NO');

  const items   = buildDubItems(latestDubTape);
  const visible = items.filter(i => dubFilters.has(i.type) || (i.type === 'run' && dubFilters.has('run')));
  latestDubViewItems = visible;

  const latestRunnable = items.find(isRunnableDubItem) || null;
  const latestRunnableKey = getRunnableDubKey(latestRunnable);
  if (latestRunnableKey && latestRunnableKey !== lastRunnableDubKey) {
    lastRunnableDubKey = latestRunnableKey;
    runnableDubFlashUntil = Date.now() + 20000;
  }

  const list  = byId('dubs-list');
  const empty = byId('dubs-empty');
  if (!list || !empty) { updateIndicators(); return; }

  const counts = { error: 0, diagnostic: 0, enhancement: 0, run: 0 };
  items.forEach(i => { if (i.type in counts) counts[i.type]++; });

  list.innerHTML = '';
  selectedDubIndex = visible.length ? Math.min(selectedDubIndex, visible.length - 1) : -1;

  const set = (id, val) => { const el = byId(id); if (el) el.textContent = String(val); };
  set('dubs-count', items.length);
  set('dubs-count-error', counts.error);
  set('dubs-count-diagnostic', counts.diagnostic);
  set('dubs-count-enhancement', counts.enhancement);
  set('dubs-count-run', counts.run);
  const tabStar = byId('dubs-tab-star');
  if (tabStar) tabStar.style.display = counts.diagnostic > 0 ? '' : 'none';

  // persistent monitor messages for tape state
  const monMsg = byId('monitor-message');
  if (!recorderRunning && !aiEngaged && !recentChatManifest && !yesItemsReady && monMsg) {
    if (counts.diagnostic > 0) monMsg.textContent = 'DIAGNOSTIC REPAIR READY';
    else if (counts.enhancement > 0) monMsg.textContent = 'ENHANCEMENTS SUGGESTED';
    else if (!proposalsAwaiting) monMsg.textContent = '';
  }

  document.querySelectorAll('#dubs-categories .dub-cat').forEach(el => {
    el.classList.toggle('inactive', !dubFilters.has(el.dataset.kind));
  });

  if (!visible.length) {
    empty.style.display = '';
    selectedDubManifest = null;
    updateIndicators();
    return;
  }

  empty.style.display = 'none';

  for (let index = 0; index < visible.length; index++) {
    const item = visible[index];
    const div = document.createElement('div');
    const itemKey = `${item.type}:${item.dubID}:${item.action}:${item.filePath}`;
    div.className = `dub-item ${item.tone}`;
    div.dataset.index = String(index);
    if (expandedDubKey === itemKey) div.classList.add('expanded');
    if (selectedDubIndex === index) div.classList.add('selected');

    div.innerHTML = `
      <span class="dub-id">${esc(String(item.dubID || '').slice(-8))}</span>
      <span>
        <div class="dub-action"><span class="dub-label ${item.tone}">${esc(item.label)}</span> ${esc(item.action || '—')}${item.filePath ? ` (${esc(item.filePath.replace(/\\/g, '/').split('/').pop())})` : ''}</div>
        ${item.task ? `<div class="dub-summary">${esc(item.task)}</div>` : ''}
        ${item.status === 'proposal' ? `<div class="dub-summary">Recorder: ${esc(item.recorderStatus || 'NO')}</div>` : ''}
        <div class="dub-summary">${esc(item.summary || '')}</div>
        <div class="dub-detail"><pre>${esc(JSON.stringify(item.detail, null, 2))}</pre></div>
      </span>
      <span class="dub-meta">${esc(item.time || '')}</span>
    `;

    div.addEventListener('click', () => {
      const willExpand = !div.classList.contains('expanded');
      list.querySelectorAll('.dub-item.expanded').forEach(el => el.classList.remove('expanded'));
      list.querySelectorAll('.dub-item.selected').forEach(el => el.classList.remove('selected'));
      div.classList.add('selected');
      selectedDubIndex = index;
      expandedDubKey = willExpand ? itemKey : null;
      if (willExpand) div.classList.add('expanded');
      syncSelectedManifest();
    });

    list.appendChild(div);
  }

  syncSelectedManifest();
}

function isAcceptedCompletedDub(item) {
  return Boolean(
    item &&
    item.status === 'done' &&
    (item.kind === 'diagnostic' || item.kind === 'enhancement') &&
    item.result &&
    (item.result.acceptedFromTape || item.result.correctedByLocalAI || item.recorderStatus === 'RETRIEVED')
  );
}

function completedDubCheck(item) {
  return isAcceptedCompletedDub(item) ? '✓ ' : '';
}

function buildDubItems(tape) {
  const out = [];
  for (const item of (tape || []).slice().reverse()) {
    const manifest = item.manifest || {}, result = item.result || {};
    const time = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '';
    const action = manifest.Action || result.kind || '—';
    const task = manifest.Task || '';
    const filePath = manifest.FilePath || result.filePath || '';

    if (item.status === 'proposal') {
      const type = item.kind === 'enhancement' ? 'enhancement' : item.kind === 'error' ? 'error' : 'diagnostic';
      out.push({
        type, tone: type, dubID: item.dubID, action, task, filePath, time,
        label: type === 'enhancement' ? 'Enhancement' : type === 'error' ? 'Error Report' : 'Diagnostic Repair',
        summary: result.summary || task || 'Proposal saved to tape.',
        source: item.source || 'eq', recorderStatus: item.recorderStatus || 'NO',
        status: item.status, manifest,
        detail: { manifest, result, recorderStatus: item.recorderStatus, source: item.source }
      });
    } else {
      const completedKind = item.kind === 'diagnostic' || item.kind === 'enhancement' ? item.kind : (item.status === 'error' ? 'error' : 'run');
      const acceptedCheck = completedDubCheck(item);
      out.push({
        type: completedKind,
        tone: completedKind,
        label: item.status === 'error'
          ? 'RUN ERROR'
          : completedKind === 'enhancement'
            ? `${acceptedCheck}Enhancement Run`
            : completedKind === 'diagnostic'
              ? `${acceptedCheck}Diagnostic Run`
              : `${acceptedCheck}RUN`,
        dubID: item.dubID, action, task, filePath, time,
        summary: item.status === 'error' ? (result.error || result.message || 'Run failed.') : (result.message || 'Execution completed.'),
        source: item.source || 'recorder', recorderStatus: item.recorderStatus || 'RETRIEVED',
        status: item.status, manifest,
        acceptedCompleted: isAcceptedCompletedDub(item),
        detail: { manifest, result, recorderStatus: item.recorderStatus, source: item.source, kind: item.kind }
      });
    }
  }
  return out;
}

document.querySelectorAll('#dubs-categories .dub-cat').forEach(el => {
  el.addEventListener('click', () => {
    const kind = el.dataset.kind;
    if (!kind) return;
    if (dubFilters.has(kind)) dubFilters.delete(kind);
    else dubFilters.add(kind);
    refreshDubTape();
  });
});

on('dubs-clear-btn', 'click', async () => {
  const data = await api('/dubs/clear', 'POST');
  if (data.ok) {
    selectedDubIndex = -1;
    expandedDubKey = null;
    selectedDubManifest = null;
    toast('Dubs tape reset. History preserved.');
    await refreshDubTape();
    await refreshTape();
  }
});

// ── HISTORY TAPE ──────────────────────────────────────────────────────────────

async function refreshTape() {
  const data = await api('/playback');
  if (!data.ok) return;
  const tape = (data.tape || []).slice().reverse();
  const list = byId('tape-list');
  if (!list) return;
  list.innerHTML = '';
  selectedTapeIndex = tape.length ? Math.min(selectedTapeIndex, tape.length - 1) : -1;
  if (!tape.length) { list.innerHTML = '<div style="color:var(--text3)">No executions recorded yet.</div>'; return; }
  for (const item of tape) {
    const m = item.manifest || {}, r = item.result || {};
    const el = document.createElement('div');
    el.className = `tape-item ${item.status} ${item.kind || ''}`;
    const dt = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '';
    const taskTitle = String(m.Task || r.task || r.summary || 'Untitled task').trim();
    const subtitle = [m.Action || item.kind || '—', m.FilePath || ''].filter(Boolean).join(' / ');
    el.innerHTML = `
      <div class="tape-row1">
        <span class="tape-dubid">${esc(String(item.dubID || '').slice(-8))}</span>
        <span class="tape-status-dot ${item.status}">${item.status === 'done' ? '✓' : item.status === 'proposal' ? '•' : '✗'}</span>
      </div>
      <div class="tape-action">${esc(taskTitle)}</div>
      <div class="tape-path">${esc(subtitle)}</div>
      <div class="tape-time">${esc(dt)}</div>
      <div class="tape-detail"><pre>${esc(JSON.stringify({ recorderStatus: item.recorderStatus, kind: item.kind, action: m.Action, filePath: m.FilePath, result: r }, null, 2))}</pre></div>
    `;
    el.addEventListener('click', () => el.classList.toggle('expanded'));
    list.appendChild(el);
  }
}

on('tape-refresh-btn', 'click', refreshTape);
on('tape-archive-btn', 'click', async () => {
  const data = await api('/playback');
  if (data.ok) downloadText(JSON.stringify(data.tape, null, 2), `tape-${Date.now()}.json`);
});

// ── LOG STREAMS ───────────────────────────────────────────────────────────────

const LOG_ENDPOINTS = { system: '/logs/system', 'ai-chat': '/logs/ai-chat', 'ai-diagnostic': '/logs/ai-diagnostic', code: '/logs/code' };

function parseLogTimestamp(line) {
  const match = String(line || '').match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/);
  if (!match) return null;
  const time = new Date(`${match[1]}T${match[2]}`);
  return Number.isFinite(time.getTime()) ? time : null;
}

function buildLogEventBlocks(lines, source) {
  const blocks = [];
  let current = null;

  for (const rawLine of lines || []) {
    const line = String(rawLine ?? '');
    const timestamp = parseLogTimestamp(line);

    if (timestamp) {
      if (current) blocks.push(current);
      current = { source, timestamp, lines: [line] };
      continue;
    }

    if (current) current.lines.push(line);
  }

  if (current) blocks.push(current);
  return blocks;
}

function toneForTimelineBlock(block) {
  const text = (block?.lines || []).join('\n').toLowerCase();
  if (text.includes('[ai:out]') || block?.source === 'AI Chat') return ' chat-out';
  if (text.includes('[ai:in]')) return ' chat-in';
  if (block?.source === 'Code Diffs') return ' code';
  if (/fail|error|missing|blocked/.test(text)) return ' fail';
  if (/ready|complete|done|pass|ok/.test(text)) return ' pass';
  return '';
}

function normalizeTimelineText(block) {
  const label = block?.source ? `[${block.source}] ` : '';
  return label + (block?.lines || []).join('\n');
}

function getHistoryWindowInfo() {
  if (historyWindowStep < HISTORY_WINDOW_STEPS.length) return HISTORY_WINDOW_STEPS[historyWindowStep];
  const months = Math.max(1, historyExtraMonths || 1);
  return { label: `${months} month${months === 1 ? '' : 's'}`, ms: months * 31 * 24 * 60 * 60 * 1000 };
}

function advanceHistoryWindow() {
  if (historyWindowStep < HISTORY_WINDOW_STEPS.length - 1) historyWindowStep++;
  else {
    historyWindowStep = HISTORY_WINDOW_STEPS.length;
    historyExtraMonths = Math.max(1, historyExtraMonths + 1);
  }
}

function updateHistoryWindowLabel() {
  const info = getHistoryWindowInfo();
  const label = byId('history-window-label');
  if (label) label.textContent = `Showing ${info.label}`;
}

function ensureHistoryMoreControls() {
  const container = byId('log-stream-timeline');
  if (!container) return;
  if (byId('history-more-btn')) { updateHistoryWindowLabel(); return; }

  const controls = document.createElement('div');
  controls.id = 'history-window-controls';
  controls.style.display = 'flex';
  controls.style.justifyContent = 'space-between';
  controls.style.alignItems = 'center';
  controls.style.gap = '8px';
  controls.style.margin = '0 0 8px';
  controls.innerHTML = '<span id="history-window-label" style="color:var(--text3);font-size:12px"></span><button type="button" id="history-more-btn" class="inp-btn">More</button>';
  container.parentElement?.insertBefore(controls, container);
  byId('history-more-btn')?.addEventListener('click', async () => {
    advanceHistoryWindow();
    await loadTimelineView();
  });
  updateHistoryWindowLabel();
}

function filterTimelineBlocksByWindow(blocks) {
  const info = getHistoryWindowInfo();
  const since = Date.now() - info.ms;
  return (blocks || []).filter(block => block?.timestamp?.getTime?.() >= since);
}

async function loadTimelineView() {
  const container = byId('log-stream-timeline');
  if (!container) return;
  ensureHistoryMoreControls();
  updateHistoryWindowLabel();

  const sources = [
    ['System', LOG_ENDPOINTS.system],
    ['AI Chat', LOG_ENDPOINTS['ai-chat']],
    ['AI Diagnostics', LOG_ENDPOINTS['ai-diagnostic']],
    ['Code Diffs', LOG_ENDPOINTS.code]
  ];

  const results = await Promise.all(sources.map(async ([label, endpoint]) => {
    const data = await api(`${endpoint}?limit=200`);
    return { label, lines: data?.ok && Array.isArray(data.lines) ? data.lines : [] };
  }));

  const allBlocks = results
    .flatMap(result => buildLogEventBlocks(result.lines, result.label))
    .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
  const blocks = filterTimelineBlocksByWindow(allBlocks);

  container.innerHTML = '';

  if (!blocks.length) {
    container.innerHTML = '<span style="color:var(--text3)">No timestamped timeline data in this window.</span>';
    return;
  }

  for (const block of blocks) {
    const div = document.createElement('div');
    div.className = 'log-stream-line timeline-line' + toneForTimelineBlock(block);
    div.textContent = normalizeTimelineText(block);
    container.appendChild(div);
  }

  container.scrollTop = container.scrollHeight;
}

async function loadLogStream(tab) {
  if (tab === 'timeline') {
    await loadTimelineView();
    return;
  }
  const endpoint = LOG_ENDPOINTS[tab];
  const container = byId(`log-stream-${tab}`);
  if (!endpoint || !container) return;
  const data = await api(endpoint);
  if (!data.ok) { container.innerHTML = `<span style="color:var(--text3)">No data yet.</span>`; return; }
  container.innerHTML = '';
  for (const line of data.lines || []) {
    const div = document.createElement('div');
    const l = String(line).toLowerCase();
    const chatTone = l.includes('[ai:out]') ? ' chat-out' : l.includes('[ai:in]') ? ' chat-in' : '';
    div.className = 'log-stream-line' + (chatTone || (l.includes('[ai:') ? ' ai' : l.includes('[code]') ? ' code' : /fail|error|missing|blocked/.test(l) ? ' fail' : /ready|complete|done|pass|ok/.test(l) ? ' pass' : ''));
    const jsonStart = String(line).indexOf('{');
    div.textContent = chatTone === ' chat-in' && jsonStart >= 0
      ? String(line).slice(0, jsonStart) + extractAIResponseText(String(line).slice(jsonStart))
      : line;
    container.appendChild(div);
  }
  container.scrollTop = container.scrollHeight;
}

on('timelinelog-refresh-btn', 'click', () => loadTimelineView());
on('timelinelog-copy-btn', 'click', () => {
  const lines = [...(byId('log-stream-timeline')?.querySelectorAll('.log-stream-line') || [])].map(el => el.textContent).join('\n');
  navigator.clipboard?.writeText(lines);
  toast('Copied!');
});
on('timelinelog-archive-btn', 'click', () => {
  const lines = [...(byId('log-stream-timeline')?.querySelectorAll('.log-stream-line') || [])].map(el => el.textContent).join('\n');
  downloadText(lines, `timeline-${Date.now()}.log`);
});

['system', 'ai-chat', 'ai-diagnostic', 'code'].forEach(key => {
  on(`${key}log-refresh-btn`, 'click', () => loadLogStream(key));
  on(`${key}log-copy-btn`, 'click', () => {
    const lines = [...(byId(`log-stream-${key}`)?.querySelectorAll('.log-stream-line') || [])].map(el => el.textContent).join('\n');
    navigator.clipboard?.writeText(lines);
    toast('Copied!');
  });
  on(`${key}log-archive-btn`, 'click', async () => {
    const data = await api(LOG_ENDPOINTS[key]);
    if (data.ok) downloadText((data.lines || []).join('\n'), `${key}-${Date.now()}.log`);
  });
});

// ── PROJECT UI ────────────────────────────────────────────────────────────────
// Project selector/cards/header/navigation live in monitorProject.js.
// These delegates keep older monitor call sites stable.

async function loadProjectSelector() {
  return await window.MonitorProject?.loadProjectSelector?.();
}

async function notifyProjectSelect(projectOrName) {
  return await window.MonitorProject?.notifyProjectSelect?.(projectOrName);
}

async function renderProjectCards(projects, activeProject) {
  return await window.MonitorProject?.renderProjectCards?.(projects, activeProject);
}

function renderProject(projectState = null) {
  return window.MonitorProject?.renderProject?.(projectState);
}

function getProjectSelectableUnits() {
  return window.MonitorProject?.getProjectSelectableUnits?.() || [];
}

function navigateProject(direction) {
  return window.MonitorProject?.navigateProject?.(direction);
}

window.MonitorProject = {
  ...(window.MonitorProject || {}),
  loadProjectSelector: window.MonitorProject?.loadProjectSelector,
  renderProject: window.MonitorProject?.renderProject,
  notifyProjectSelect: window.MonitorProject?.notifyProjectSelect,
  renderProjectCards: window.MonitorProject?.renderProjectCards,
  getProjectSelectableUnits: window.MonitorProject?.getProjectSelectableUnits,
  navigateProject: window.MonitorProject?.navigateProject
};

function renderCustomProjectSettingsList() {
  const list = byId('set-custom-projects-list');
  if (!list) return;
  list.innerHTML = '';

  if (!projectSettings.length) {
    list.innerHTML = '<div style="color:var(--text3);font-size:11px">No custom project folders added.</div>';
    return;
  }

  projectSettings.forEach((project, index) => {
    const row = document.createElement('div');
    row.style.display = 'flex';
    row.style.alignItems = 'center';
    row.style.justifyContent = 'space-between';
    row.style.gap = '10px';
    row.style.padding = '8px 10px';
    row.style.border = '1px solid var(--border2)';
    row.style.borderRadius = 'var(--radius)';
    row.style.background = 'var(--bg2)';
    row.innerHTML = `
      <div style="min-width:0">
        <div style="font-size:12px;color:var(--text1)">${esc(project.name || '')}</div>
        <div style="font-size:11px;color:var(--text2);word-break:break-all">${esc(project.location || project.path || '')}</div>
      </div>
      <button class="inp-btn" data-custom-project-remove="${index}">REMOVE</button>
    `;
    list.appendChild(row);
  });

  list.querySelectorAll('[data-custom-project-remove]').forEach(btn => {
    btn.addEventListener('click', () => {
      const index = Number(btn.getAttribute('data-custom-project-remove'));
      if (!Number.isFinite(index)) return;
      projectSettings.splice(index, 1);
      renderCustomProjectSettingsList();
    });
  });
}

function collectProjectsFromSettings() {
  return projectSettings.map(project => ({
    name: String(project?.name || '').trim(),
    location: String(project?.location || project?.path || '').trim()
  })).filter(project => project.name && project.location);
}
function settingsSectionHeadByText(text) {
  const wanted = String(text || '').trim().toLowerCase();
  return [...document.querySelectorAll('#settings-form .section-head')]
    .find(head => String(head.textContent || '').trim().toLowerCase() === wanted) || null;
}

function collectSettingsSectionNodes(head) {
  const nodes = [];
  if (!head) return nodes;
  let node = head;
  while (node) {
    nodes.push(node);
    node = node.nextElementSibling;
    if (node?.classList?.contains('section-head')) break;
    if (node?.querySelector?.('#settings-save-btn')) {
      nodes.push(node);
      break;
    }
  }
  return nodes;
}

function renameSettingsSection(oldText, nextText) {
  const head = settingsSectionHeadByText(oldText);
  if (head) head.textContent = nextText;
}

function reorderSettingsDisplay() {
  const form = byId('settings-form');
  if (!form || form.dataset.reordered === 'true') return;

  renameSettingsSection('Project Guardrails', 'Recorder Settings');
  renameSettingsSection('AUTO Play Handling', 'AUTO Play Handling');
  renameSettingsSection('Custom Project Folders', 'Projects');

  const saveRow = byId('settings-save-btn')?.closest('.save-row') || null;
  const account = collectSettingsSectionNodes(settingsSectionHeadByText('Account'));
  const beta = collectSettingsSectionNodes(settingsSectionHeadByText('Beta Release'));
  const ai = collectSettingsSectionNodes(settingsSectionHeadByText('AI Backend'));
  const deck = collectSettingsSectionNodes(settingsSectionHeadByText('Deck'));
  const connection = collectSettingsSectionNodes(settingsSectionHeadByText('Connection Grid'));
  const guardrails = collectSettingsSectionNodes(settingsSectionHeadByText('Recorder Settings'));
  const autoPlay = collectSettingsSectionNodes(settingsSectionHeadByText('AUTO Play Handling'));
  const projects = collectSettingsSectionNodes(settingsSectionHeadByText('Projects'));

  const recorderHead = guardrails[0] || null;
  if (recorderHead) recorderHead.textContent = 'Recorder Settings';

  const scriptsRow = document.createElement('div');
  scriptsRow.className = 'field-row';
  scriptsRow.innerHTML = '<span class="field-label">Custom Scripts</span><div style="padding-top:6px;font-size:12px;color:var(--text3)">Coming soon</div>';

  const ordered = [account, beta, ai, deck, connection, guardrails, autoPlay, [scriptsRow], projects]
    .flat()
    .filter(Boolean)
    .filter(node => node !== saveRow);

  ordered.forEach(node => form.appendChild(node));
  if (saveRow) form.appendChild(saveRow);
  form.dataset.reordered = 'true';
}

async function loadSettingsUI() {
  const settings = await SettingsStore.load();
  if (!settings) return;

  const setVal = (id, value) => {
    const el = byId(id);
    if (el) el.value = value == null ? '' : String(value);
  };
  const setChk = (id, value) => {
    const el = byId(id);
    if (el) el.checked = Boolean(value);
  };

  setVal('set-ai-url', settings.lmStudioUrl || 'http://127.0.0.1:1234');
  setVal('set-ai-model', settings.model || '');
  setVal('set-deck-url', DeckConnection.getURL());
  setVal('set-port-mode', settings.server?.portMode || 'random');
  setVal('set-port-number', settings.server?.portMode === 'fixed' ? (settings.server?.port || settings.server?.runtimePort || '') : '');
  setVal('set-ngrok-url', settings.bridge?.url || '');
  setVal('set-release-version', `${settings.release?.channel || 'beta'} v${settings.release?.version || ''}`.trim());
  setChk('set-auto-update', true);

  const aph = settings.autoPlayHandling || {};
  setChk('set-auto-chat-manifests', aph.chatManifests);
  setChk('set-auto-diagnostic-repair', aph.diagnosticRepair);
  setChk('set-auto-enhancement-suggestions', aph.enhancementSuggestions);

  projectSettings = Array.isArray(settings.projects)
    ? settings.projects.map(project => ({
        name: String(project?.name || '').trim(),
        location: String(project?.location || project?.path || '').trim()
      })).filter(project => project.name && project.location)
    : [];

  setVal('set-auth-email', settings.authEmail || '');
  setAccountView({
    userId: settings.authUserId,
    email: settings.authEmail,
    tier: settings.authTier,
    status: settings.authStatus
  });

  renderCustomProjectSettingsList();
  reorderSettingsDisplay();
}


on('set-custom-project-add-btn', 'click', () => {
  const nameInput = byId('set-custom-project-name');
  const pathInput = byId('set-custom-project-path');
  const name = nameInput?.value.trim() || '';
  const projectPath = pathInput?.value.trim() || '';

  if (!name || !projectPath) {
    toast('Enter both a project name and folder path');
    return;
  }

  projectSettings.push({ name, location: projectPath });
  if (nameInput) nameInput.value = '';
  if (pathInput) pathInput.value = '';
  renderCustomProjectSettingsList();
});

on('copy-amp-action-script-btn', 'click', async () => {
  window.open('/assets/docs/Nodarama Go and Verbatim AMP Setup for ChatGPT.txt', '_blank', 'noopener');
  toast('AMP setup opened.');
});

on('settings-save-btn', 'click', async () => {
  const newURL = byId('set-deck-url')?.value.trim();
  if (newURL) {
    SNP.setDeckURL(newURL);
    DECK_URL = DeckConnection.getURL();
  }

  const result = await SettingsStore.save({
    lmStudioUrl: byId('set-ai-url')?.value.trim() || 'http://127.0.0.1:1234',
    server: {
      host: '127.0.0.1',
      portMode: byId('set-port-mode')?.value === 'fixed' ? 'fixed' : 'random',
      port: byId('set-port-mode')?.value === 'fixed' ? Number(byId('set-port-number')?.value || 0) : 0,
      runtimePort: 0
    },
    bridge: {
      type: 'ngrok',
      url: byId('set-ngrok-url')?.value.trim() || '',
      autoStart: Boolean(byId('set-ngrok-url')?.value.trim())
    },
    release: { channel: 'beta', version: '', autoUpdate: true },
    projects: collectProjectsFromSettings(),
    autoPlayHandling: {
      chatManifests: Boolean(byId('set-auto-chat-manifests')?.checked),
      diagnosticRepair: Boolean(byId('set-auto-diagnostic-repair')?.checked),
      enhancementSuggestions: Boolean(byId('set-auto-enhancement-suggestions')?.checked)
    }
  });

  if (!result?.ok) {
    toast('Settings save failed: ' + (result?.error || 'unknown'));
    return;
  }

  await loadProjectSelector();
  await refreshStatus();
  toast('Settings saved.');
});

async function applyInitialTabState() {
  const auth = await refreshAccountUI();
  const initialTab = resolveDefaultTabFromAuth(auth || {});
  activateTab(initialTab);
  return auth;
}
function getSelectedTierChoice() {
  return document.querySelector('input[name="account-tier-choice"]:checked')?.value || 'limited';
}

function setModalDisplay(id, visible) {
  const el = byId(id);
  if (el) el.style.display = visible ? 'flex' : 'none';
}

function openTierChoiceModal() {
  setModalDisplay('tier-choice-modal', true);
}

function closeTierChoiceModal() {
  setModalDisplay('tier-choice-modal', false);
}

function openActivationWaitModal(message, portalUrl = '') {
  pendingActivationUrl = portalUrl || pendingActivationUrl || ACCOUNT_PORTAL_URL;
  const msg = byId('activation-wait-message');
  if (msg) msg.textContent = message || 'Complete activation in Nodarama Accounts Manager.';
  setModalDisplay('activation-wait-modal', true);
}

function closeActivationWaitModal() {
  setModalDisplay('activation-wait-modal', false);
}

function buildAccountsPortalUrl(payload) {
  const url = new URL(payload?.activationUrl || ACCOUNT_PORTAL_URL);
  if (payload?.activationId) url.searchParams.set('activationId', payload.activationId);
  const email = byId('set-auth-email')?.value.trim();
  if (email) url.searchParams.set('email', email);
  return url.toString();
}

function saveSignedInAccount(result, fallbackEmail) {
  return SettingsStore.save({
    authEmail: result.user?.email || fallbackEmail || '',
    authUserId: result.user?.userId || result.user?.id || '',
    authTier: result.user?.accessTier || result.user?.tier || result.accessTier || result.tier || 'limited',
    authStatus: 'signed_in'
  });
}

async function finishActivatedLogin(result, fallbackEmail) {
  await saveSignedInAccount(result, fallbackEmail);

  if (byId('set-auth-password')) byId('set-auth-password').value = '';

  pendingActivationId = '';
  pendingActivationUrl = '';

  if (activationPollTimer) {
    clearInterval(activationPollTimer);
    activationPollTimer = null;
  }

  closeActivationWaitModal();

  const auth = await refreshAccountUI();
  await loadSettingsUI();
  await loadProjectSelector();

  LaunchState.auth = auth;
  LaunchState.settings = await SettingsStore.load();
  LaunchState.backstage = isBackstageAuth(LaunchState.auth);
  LaunchState.projects = await fetchLaunchProjects();

  await renderLaunchState();

  toast('Account activated');
}

async function pollActivationStatus() {
  if (!pendingActivationId) return;
  const result = await api('/auth/activation-status', 'POST', { activationId: pendingActivationId });
  if (!result?.ok) return;
  if (result.active && result.user) {
    await finishActivatedLogin(result, byId('set-auth-email')?.value.trim() || '');
    return;
  }
  if (result.message) {
    const msg = byId('activation-wait-message');
    if (msg) msg.textContent = String(result.message);
  }
}

on('tier-choice-cancel-btn', 'click', () => closeTierChoiceModal());on('agreement-login-btn', 'click', () => agreementGateLogin(false));
on('agreement-create-account-btn', 'click', () => agreementGateLogin(true));

on('activation-close-btn', 'click', () => closeActivationWaitModal());
on('activation-open-portal-btn', 'click', () => {
  const url = pendingActivationUrl || ACCOUNT_PORTAL_URL;
  window.open(url, '_blank', 'noopener');
});

on('tier-choice-confirm-btn', 'click', async () => {
  const email = byId('set-auth-email')?.value.trim() || '';
  const password = byId('set-auth-password')?.value || '';
  if (!email || !password) {
    toast('Enter email and password');
    return;
  }

  const result = await api('/auth/register', 'POST', {
    email,
    password,
    deviceName: navigator.platform || 'NV Station',
    deviceFingerprint: navigator.userAgent || 'unknown'
  });

  if (!result?.ok) {
    const message = formatAuthFailure(result, 'Create account failed');
    toast(message, 6500);
    appendLog('✗ create account failed: ' + message, 'fail');
    return;
  }

  closeTierChoiceModal();

  if (result.activationRequired) {
    pendingActivationId = String(result.activationId || '');
    pendingActivationUrl = buildAccountsPortalUrl(result);
    openActivationWaitModal(result.message || 'Complete activation in Nodarama Accounts Manager.', pendingActivationUrl);
    window.open(pendingActivationUrl, '_blank', 'noopener');
    if (activationPollTimer) clearInterval(activationPollTimer);
    activationPollTimer = setInterval(() => { void pollActivationStatus(); }, 3000);
    return;
  }

  if (result.user) {
    await finishActivatedLogin(result, email);
  }
});

// ── STATUS ────────────────────────────────────────────────────────────────────

async function refreshStatus() {
  const data = await api('/status');
  if (!data.ok) {
    recorderRunning = false;
    const ms = byId('monitor-status');
    if (ms) ms.textContent = '● OFFLINE';
    updateIndicators();
    return;
  }
  recorderRunning = data.state === 'RECORD';
  const ms = byId('monitor-status');
  if (ms) ms.textContent = data.state === 'RECORD' ? '● REC' : data.state === 'PAUSE' ? '● PAUSE' : '● IDLE';
  const dot = byId('deck-status-dot'), label = byId('deck-state-label');
  if (dot) dot.className = (data.state || 'stop').toLowerCase();
  if (label) label.textContent = data.state || 'STOP';
  updateIndicators();
}

async function refreshAll() {
  await refreshStatus();
  appendLog('↻ refreshed', 'neutral');
  toast('Refreshed.');
}

// ── SIDEBAR LOG ───────────────────────────────────────────────────────────────

function renderLogLineContent(row, text) {
  const raw = String(text || '').trim();
  const match = raw.match(/^(TASK(?: COMPLETE| FAILED)?\s*→)\s*(.*?)\s+@\s+([^—]+)(?:\s+—\s+(.*))?$/i);
  if (!match) {
    row.textContent = raw;
    return;
  }

  const [, prefix, task, filePath, detail] = match;
  row.classList.add('task-log-line');
  row.innerHTML = `
    <span class="log-task-prefix">${esc(prefix)}</span>
    <span class="log-task-name">${esc(task || 'task')}</span>
    <span class="log-task-at"> @ </span>
    <span class="log-file-path">${esc(filePath || '(no file)')}</span>
    ${detail ? `<span class="log-error-detail"> — ${esc(detail)}</span>` : ''}
  `;
}

function appendLog(text, tone = 'neutral') {
  const log = byId('system-log');
  if (!log) { console.warn('[appendLog]', tone, text); return; }
  const ph = byId('log-placeholder');
  if (ph) ph.remove();
  logLines.push({ text, tone });
  if (logLines.length > 300) logLines.shift();
  const row = document.createElement('div');
  row.className = `log-line ${tone}`;
  renderLogLineContent(row, new Date().toLocaleTimeString('en', { hour12: false }) + ' ' + text);
  log.appendChild(row);
  while (log.children.length > 300) log.firstChild.remove();
  log.scrollTop = log.scrollHeight;
}

function stripSidebarFieldLabels(text) {
  return String(text || '')
    .replace(/\b([A-Za-z][A-Za-z0-9_]*):([^\s][^\n]*?)(?=\s+[A-Za-z][A-Za-z0-9_]*:|$)/g, (_match, key, value) => {
      const cleanKey = String(key || '').trim().toLowerCase();
      const cleanValue = String(value || '').trim();
      if (!cleanValue) return '';
      if (['event', 'source'].includes(cleanKey)) return cleanValue;
      return cleanValue;
    })
    .replace(/\s{2,}/g, ' ')
    .trim();
}

function formatSidebarExecutionLine(text) {
  const clean = String(text || '').trim();
  if (!clean) return '';

  const pick = key => {
    const match = clean.match(new RegExp(`${key}:([^\n]+?)(?=\s+[A-Za-z][A-Za-z0-9_]*:|$)`, 'i'));
    return match ? match[1].trim() : '';
  };

  const event = clean.split(/\s+/)[0] || '';
  const dubID = pick('dubID');
  const filePath = pick('filePath') || pick('FilePath');
  const action = pick('Action') || 'task';
  const error = pick('error') || pick('rawError') || pick('normalizedReason');
  const file = filePath ? filePath.replace(/\\/g, '/').split('/').pop() : '(no file)';

  if (dubID && /^execute_start$/i.test(event)) {
    sidebarRunMeta.set(dubID, { file, action, filePath });
    return '';
  }

  if (/^execute$/i.test(event)) return '';

  if (dubID && /^execute_done$/i.test(event)) {
    const meta = sidebarRunMeta.get(dubID) || { file, action };
    sidebarRunMeta.delete(dubID);
    return `✓ ${meta.action || action} ${meta.file || file}`;
  }

  if (dubID && /^(execute_failure_detected|execute_error_surfaced)$/i.test(event)) {
    const meta = sidebarRunMeta.get(dubID) || { file, action };
    return `✗ ${meta.action || action} ${meta.file || file}${error ? ` — ${error}` : ''}`;
  }

  if (/^aiRepair_no_safe_repair$/i.test(event)) return '⚠ AI repair checked — no safe match';
  if (/^aiRepair_apply_failure$/i.test(event)) return `⚠ AI repair failed${error ? ` — ${error}` : ''}`;
  if (/^aiRepair_apply_start$/i.test(event)) {
    const adjustedAction = pick('adjustedAction') || action;
    const adjustedFilePath = pick('adjustedFilePath') || filePath;
    const adjustedFile = adjustedFilePath ? adjustedFilePath.replace(/\\/g, '/').split('/').pop() : file;
    return `↻ AI repair applying ${adjustedAction || 'task'} ${adjustedFile || ''}`.trim();
  }
  if (/^aiRepair_reply_received$/i.test(event)) {
    const status = pick('status');
    const confidence = pick('confidence');
    if (status === 'repair_prepared') return '⚠ AI repair prepared';
    if (status) return `⚠ AI repair ${status}`;
    if (confidence && Number(confidence) <= 0) return '⚠ AI repair had no confidence';
    return '';
  }

  if (/^aiRepair_/i.test(event)) return '';
  return '';
}
function pickLogValue(text, key) {
  const clean = String(text || '');
  const match = clean.match(new RegExp(`${key}:([^\\n]+?)(?=\\s+[A-Za-z][A-Za-z0-9_]*:|$)`));
  return match ? match[1].trim() : '';
}

function formatStructuredSidebarEvent(line) {
  const raw = String(line || '').trim();
  const clean = raw
    .replace(/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, '')
    .trim();

  const source = clean.match(/^\[([^\]]+)\]/)?.[1] || '';
  const body = clean.replace(/^\[[^\]]+\]\s+/, '').trim();
  const event = pickLogValue(body, 'event') || body.split(/\s+/)[0] || '';

  if (source === 'Hypauthcall' && /login_failed|register_failed/i.test(event)) {
    const action = /register/i.test(event) ? 'Create account failed' : 'Login failed';
    const error = pickLogValue(body, 'remoteError') || pickLogValue(body, 'error') || 'failed';
    const type = pickLogValue(body, 'failureType').replace(/_/g, ' ');
    const status = pickLogValue(body, 'status');
    const connected = pickLogValue(body, 'connected');
    return `${action} → ${[error, type, status, connected].filter(Boolean).join(' · ')}`;
  }

  if (source === 'Manifold' && /route_fallback|route_error/i.test(event)) {
    const via = pickLogValue(body, 'via');
    const reason = pickLogValue(body, 'reason') || pickLogValue(body, 'error') || 'fallback';
    return `Route fallback → ${[via, reason].filter(Boolean).join(' · ')}`;
  }

  if (source === 'Manifold' && /route_received/i.test(event)) {
    if (/route_received_repaired/i.test(event) && !/offline:true|fallback|error/i.test(body)) return '';
    const via = pickLogValue(body, 'via');
    const strategy = pickLogValue(body, 'strategy');
    return `Route received → ${[via, strategy].filter(Boolean).join(' · ')}`;
  }

  if (source === 'Hypauthcall' && /login\b|register\b/i.test(event)) {
    const tier = pickLogValue(body, 'tier');
    return `${event === 'register' ? 'Account created' : 'Signed in'}${tier ? ` → ${tier}` : ''}`;
  }

  return '';
}

function sanitizeSidebarLine(line) {
  const raw = String(line || '').trim();
  if (!raw) return '';

  const structured = formatStructuredSidebarEvent(raw);
  if (structured) return structured;

  const clean = raw
    .replace(/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, '')
    .replace(/^\[Manifest:[^\]]+\]\s+/, '')
    .replace(/^\[Deck\]\s+/, '')
    .replace(/^\[Recorder\]\s+/, '')
    .replace(/^\[EQ\]\s+/, 'EQ ')
    .trim();

  if (!clean || clean === 'execute') return '';

  if (/^\[Manifold\]\s+.*route_(request|received|received_repaired)\b/i.test(clean)) return '';
  if (/\bhypToken:|\bcontextKeys:|\btier:backstage\b/i.test(clean)) return '';
  if (/^[\[{]/.test(clean) || /^".*":\s*/.test(clean) || /^}/.test(clean)) return '';
  if (clean.includes('"Payload"') || clean.includes('search":') || clean.includes('target":') || clean.includes('replacement":')) return '';

  if (/^(active_on|active_off|queued|dequeued|queued_while_paused)\b/i.test(clean)) return '';
  if (/\b(state:|busy:|paused:|pauseRequested:|queueLength:)\b/.test(clean)) return '';
  if (/^(pause_requested|pause_cleared|pause_engaged|pause_entered_idle|resume_requested|remaining_tasks_cancelled)\b/i.test(clean)) return '';
  if (/^(record_usage_notice|server_run_gate_allowed|settings_saved)\b/i.test(clean)) return '';
  if (/^project:.*\bexecute\b/i.test(clean) || /^project:.*\baiRepair_/i.test(clean)) return formatSidebarExecutionLine(clean.replace(/^project:[^\s]+\s+/, ''));
  if (/^(execute_start|execute_done|execute_error|execute_failure_detected|execute_error_surfaced|aiRepair_)\b/i.test(clean)) return formatSidebarExecutionLine(clean);

  return stripSidebarFieldLabels(clean);
}
async function refreshSystemLog() {
  const data = await api('/logs/system');
  const log = byId('system-log');
  if (!log || !data.ok || !Array.isArray(data.lines)) return;
  const lines = data.lines.slice(-200);

  if (!systemLogSeeded) {
    systemLogSeeded = true;
    lastSidebarLine = lines.length ? lines[lines.length - 1] : '';
    lastSidebarActivityAt = Date.now();
    log.innerHTML = '<div id="log-placeholder">Waiting for new system activity…</div>';
    return;
  }

  let newLines = lines;
  if (lastSidebarLine) {
    const idx = lines.lastIndexOf(lastSidebarLine);
    newLines = idx >= 0 ? lines.slice(idx + 1) : lines.slice(-1);
  }
  if (!newLines.length) return;

  const ph = byId('log-placeholder');
  if (ph) ph.remove();

  let appended = 0;
  for (const line of newLines) {
    const clean = sanitizeSidebarLine(line);
    if (!clean) continue;
    const lineTs = parseTimestampMs(line);
    if (lineTs && (!currentSidebarGroupTime || lineTs - currentSidebarGroupTime > ACTIVITY_CLUSTER_MS)) {
      currentSidebarGroup = `system-group-time-${lineTs}`;
    }
    if (lineTs) currentSidebarGroupTime = lineTs;
    const dubMatch = String(line).match(/dubID[:=]([0-9a-z]+)/i);
    if (dubMatch) currentSidebarGroup = `system-group-${dubMatch[1]}`;
    else if (/execute_start|record\/check|approved|playback\/approve/i.test(line)) {
      currentSidebarGroup = `system-group-auto-${++systemLogGroupCounter}`;
    }
    const l = clean.toLowerCase();
    const tone = /fail|error|missing|blocked/.test(l) ? 'fail' : /ready|complete|done|pass|ok/.test(l) ? 'pass' : 'neutral';
    const row = document.createElement('div');
    row.className = `log-line ${tone}`;
    renderLogLineContent(row, clean);
    row.dataset.runGroup = currentSidebarGroup;
    log.appendChild(row);
    appended++;
  }

  if (appended > 0) lastSidebarActivityAt = Date.now();
  if (!log.children.length) log.innerHTML = '<div id="log-placeholder">Waiting for new system activity…</div>';
  while (log.children.length > 200) log.firstChild.remove();
  lastSidebarLine = newLines[newLines.length - 1];
  log.scrollTop = log.scrollHeight;

  const inactive = Date.now() - lastSidebarActivityAt >= 1800;
  log.classList.toggle('inactive', inactive);
  const rows = [...log.querySelectorAll('.log-line')];
  if (inactive && rows.length > 1) {
    rows.forEach((r, i) => {
      if (r.classList.contains('chat-in') || r.classList.contains('chat-out')) r.style.opacity = '1';
      else r.style.opacity = String(Math.max(0.08, 0.08 + (i / (rows.length - 1)) * 0.92));
    });
  } else {
    rows.forEach(r => r.style.opacity = '1');
  }
}

on('log-clear-btn', 'click', async () => {
  await api('/logs/system/clear', 'POST');
  const log = byId('system-log');
  if (log) log.innerHTML = '<div id="log-placeholder">Waiting for new system activity…</div>';
  lastSidebarLine = ''; lastSidebarActivityAt = Date.now(); systemLogSeeded = true;
});

// ── EQ CANVAS ─────────────────────────────────────────────────────────────────

const canvas = byId('eq-canvas');
const ctx2d  = canvas?.getContext('2d');
const BARS   = 18;
let eqH = Array.from({ length: BARS }, () => Math.random() * 0.22);
let eqT = Array.from({ length: BARS }, () => Math.random() * 0.26);
let eqSequence = null;
let eqSustain = null;
let lastCompletedDubKey = '';

const EQ_SEQUENCE_LIBRARY = {
  record: [
    { energy: 'M', volume: 'loud', ms: 2000 },
    { energy: 'X', volume: 'max', ms: 500 },
    { energy: 'H', volume: 'max', ms: 2000 }
  ],
  chat: [
    { energy: 'H', volume: 'max', ms: 1500 },
    { energy: 'M', volume: 'loud', ms: 1500 },
    { energy: 'LOW', volume: 'soft', ms: 1500 }
  ],
  readFile: [
    { energy: 'M', volume: 'loud', ms: 2000 },
    { energy: 'X', volume: 'max', ms: 500 },
    { energy: 'M', volume: 'loud', ms: 2000 }
  ],
  approve: [
    { energy: 'H', volume: 'max', ms: 500 },
    { energy: 'M', volume: 'loud', ms: 2000 },
    { energy: 'M', volume: 'loud', ms: 2000 }
  ],
  settleMedium: [
    { energy: 'M', volume: 'loud', ms: 2000 },
    { energy: 'LOW', volume: 'soft', ms: 2000 }
  ],
  settleLow: [
    { energy: 'LOW', volume: 'soft', ms: 2000 }
  ]
};

function startEQSequence(name, sustain = null) {
  const phases = EQ_SEQUENCE_LIBRARY[name];
  if (!Array.isArray(phases) || !phases.length) return;
  eqSequence = { name, phases, startedAt: Date.now(), sustain };
  eqSustain = sustain;
}

function clearEQSustain(reason = '') {
  if (!reason || eqSustain === reason) eqSustain = null;
}

function getEQSequencePhase() {
  if (!eqSequence) return null;
  const elapsed = Date.now() - eqSequence.startedAt;
  let cursor = 0;
  for (const phase of eqSequence.phases) {
    cursor += Math.max(0, Number(phase.ms) || 0);
    if (elapsed <= cursor) return phase;
  }
  eqSequence = null;
  return null;
}

function triggerEQActivity(level = 'medium', durationMs = 3000) {
  const normalized = String(level || '').toLowerCase();
  if (normalized === 'record') return startEQSequence('record', 'record');
  if (normalized === 'chat') return startEQSequence('chat', 'chat');
  if (normalized === 'readfile') return startEQSequence('readFile', null);
  if (normalized === 'approve') return startEQSequence('approve', 'approve');
  if (normalized === 'low') return startEQSequence('settleLow', null);
  return startEQSequence(normalized === 'high' ? 'approve' : 'settleMedium', null);
}


function getEQActivity() {
  const phase = getEQSequencePhase();
  if (phase) return phase;

  if (recorderRunning) return { energy: 'H', volume: 'max' };
  if (aiEngaged) return { energy: 'LOW', volume: 'soft' };
  if (eqSustain === 'record') return { energy: 'H', volume: 'max' };
  if (eqSustain === 'chat') return { energy: 'LOW', volume: 'soft' };
  if (eqSustain === 'approve') return { energy: 'M', volume: 'loud' };
  if (yesItemsReady) return { energy: 'M', volume: 'loud' };
  if (proposalsAwaiting || recentChatManifest) return { energy: 'M', volume: 'loud' };
  return { energy: 'L', volume: 'standard' };
}

function eqBarColor(volume, ratio) {
  const pulse = Math.max(0, Math.min(1, Number(volume || 0)));
  const r = Math.max(0, Math.min(1, Number(ratio || 0)));

  if (r >= 0.85) return pulse > 0.5 ? '#ff4d00' : '#ff7a3d';
  if (r >= 0.65) return pulse > 0.5 ? '#ffb300' : '#d9ff1c';
  if (r >= 0.40) return pulse > 0.5 ? '#d9ff1c' : '#76ff1f';
  return pulse > 0.5 ? '#18ff3a' : '#57d95d';
}
function eqTick() {
  if (!canvas || !ctx2d) return;

  canvas.width = canvas.offsetWidth;
  canvas.height = canvas.offsetHeight;

  const { energy, volume } = getEQActivity();
  const range = energy === 'X'
    ? { min: 0.85, max: 1.0 }
    : energy === 'H'
    ? { min: 0.65, max: 1.0 }
    : energy === 'M'
    ? { min: 0.4, max: 0.8 }
    : energy === 'LOW'
    ? { min: 0.25, max: 0.55 }
    : { min: 0.06, max: 0.28 };

  const w = canvas.width;
  const h = canvas.height;
  const barW = Math.floor(w / BARS);
  const gap = 2;
  const maxH = h - 14;

  ctx2d.clearRect(0, 0, w, h);

  for (let i = 0; i < BARS; i++) {
    const jitter = energy === 'H' ? 0.18 : energy === 'M' ? 0.11 : 0.05;

    if (Math.random() < 0.08 + jitter) {
      eqT[i] = range.min + Math.random() * (range.max - range.min);
    }

    eqH[i] += (eqT[i] - eqH[i]) * (energy === 'H' ? 0.24 : energy === 'M' ? 0.18 : 0.1);

    const ratio = Math.max(0.03, Math.min(1, eqH[i]));
    const barH = Math.max(8, ratio * maxH);
    const x = i * barW + gap / 2;

    const segmentHeight = 6;
    const segmentGap = 2;
    const segments = Math.floor(barH / (segmentHeight + segmentGap));

    for (let s = 0; s < segments; s++) {
      const y = h - ((s + 1) * (segmentHeight + segmentGap)) - 6;
      const segRatio = s / Math.max(segments, 1);

      const fill = eqBarColor(volume, segRatio);

      ctx2d.fillStyle = fill;
      ctx2d.globalAlpha = volume === 'max' ? 0.96 : volume === 'loud' ? 0.9 : 0.78;
      ctx2d.fillRect(x, y, barW - gap, segmentHeight);
    }
  }

  ctx2d.globalAlpha = 1;
  requestAnimationFrame(eqTick);
}
function startPolling() {
  pollTimer = setInterval(async () => {
    if (pollBusy) return;
    pollBusy = true;
    pollCycle++;
    try {
      const wasRunning = recorderRunning;
      await refreshStatus();
      await refreshSystemLog();
      if (pollCycle % 2 === 0 || wasRunning || recorderRunning) await refreshDubTape();
      if (wasRunning && !recorderRunning) {
        clearEQSustain('record');
        triggerEQActivity('medium');
        setTemporaryMessage('✓ RUN COMPLETE', 5000);
        await refreshDubTape();
        await refreshTape();
      }
    } finally {
      pollBusy = false;
    }
  }, 3000);
}

// ── ACCOUNT BUTTONS ───────────────────────────────────────────────────────────

on('account-login-form', 'submit', event => {
  event.preventDefault();
  byId('account-login-btn')?.click();
});
on('agreement-login-form', 'submit', event => {
  event.preventDefault();
  byId('agreement-login-btn')?.click();
});

on('account-login-btn', 'click', async () => {
  const email = byId('set-auth-email')?.value.trim() || '';
  const password = byId('set-auth-password')?.value || '';
  if (!email || !password) { toast('Enter email and password'); return; }
  const result = await api('/auth/login', 'POST', { email, password });
  if (!result?.ok) {
    const message = formatAuthFailure(result, 'Login failed');
    toast(message, 6500);
    appendLog('✗ login failed: ' + message, 'fail');
    return;
  }
  await finishActivatedLogin(result, email);
  toast('Signed in');
});

on('account-register-btn', 'click', () => openTierChoiceModal());

on('account-logout-btn', 'click', async () => {
  const result = await api('/auth/logout', 'POST');
  if (!result?.ok) { toast('Logout failed'); return; }
  await SettingsStore.save({ authStatus: 'signed_out', authTier: 'limited', authEmail: '', authUserId: '' });
  await refreshAccountUI();
  await loadSettingsUI();
  toast('Signed out');
});

// ── INIT ──────────────────────────────────────────────────────────────────────

(async function init() {
  appendLog('⚡ NV monitor starting', 'pass');

  try { eqTick(); }
  catch (err) { console.error('[monitor eq failed]', err); }

  try {
    DECK_URL = DeckConnection.getURL();
    const deckInput = byId('set-deck-url');
    if (deckInput) deckInput.value = DECK_URL;
  } catch (err) {
    appendLog('✗ deck URL init failed: ' + (err?.message || err), 'fail');
  }

  try {
    wireLaunchGateControls();
  } catch (err) {
    appendLog('✗ launch gate wiring failed: ' + (err?.message || err), 'fail');
  }

  try { await loadSettingsUI(); }
  catch (err) { appendLog('✗ settings load failed: ' + (err?.message || err), 'fail'); }

  try {
    await loadProjectSelector();
  } catch (err) {
    appendLog('✗ project selector load failed: ' + (err?.message || err), 'fail');
  }

  try {
    await bootLaunchGate();
  } catch (err) {
    appendLog('✗ launch gate failed: ' + (err?.message || err), 'fail');
  }

  try { await refreshStatus(); }
  catch (err) { appendLog('✗ status refresh failed: ' + (err?.message || err), 'fail'); }

  try { await refreshSystemLog(); }
  catch (err) { appendLog('✗ system log refresh failed: ' + (err?.message || err), 'fail'); }

  try { await refreshDubTape(); }
  catch (err) { appendLog('✗ dubs refresh failed: ' + (err?.message || err), 'fail'); }

  try { startPolling(); }
  catch (err) { console.error('[monitor polling failed]', err); }

  appendLog('⚡ NV monitor ready', 'pass');
})();