← back to Build Debrief

public/index.html

490 lines

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Build Debrief — Steve's project board</title>
  <script>
    (function() {
      try {
        const t = localStorage.getItem('debrief-theme') || 'dark';
        document.documentElement.setAttribute('data-theme', t);
      } catch {}
    })();
  </script>
  <style>
    :root[data-theme="dark"] {
      --bg: #0c0e13; --panel: #14181f; --panel-2: #1b2029; --line: rgba(255,255,255,0.08);
      --fg: #e7ecf2; --muted: rgba(231,236,242,0.55); --accent: #ffb000;
      --pro-bg: rgba(46,204,113,0.10); --pro-border: rgba(46,204,113,0.32); --pro-fg: #2ecc71;
      --con-bg: rgba(231,76,60,0.10); --con-border: rgba(231,76,60,0.32); --con-fg: #ff6b5a;
      --hot: #ff6b5a; --warm: #ffb000; --cool: #4ea1ff; --cold: #6c7480;
      --kbd-bg: rgba(255,255,255,0.06);
    }
    :root[data-theme="light"] {
      --bg: #f4f6fb; --panel: #fff; --panel-2: #eef1f6; --line: rgba(20,24,31,0.10);
      --fg: #14181f; --muted: rgba(20,24,31,0.55); --accent: #c46b00;
      --pro-bg: rgba(46,160,60,0.08); --pro-border: rgba(46,160,60,0.32); --pro-fg: #1e8a3a;
      --con-bg: rgba(200,30,30,0.08); --con-border: rgba(200,30,30,0.32); --con-fg: #c0392b;
      --hot: #c0392b; --warm: #c46b00; --cool: #1e6cb0; --cold: #748090;
      --kbd-bg: rgba(20,24,31,0.06);
    }
    * { box-sizing: border-box; }
    html, body { margin: 0; padding: 0; height: 100%; background: var(--bg); color: var(--fg); font: 13.5px/1.45 -apple-system, system-ui, "Segoe UI", sans-serif; }
    button, input, textarea, select { font: inherit; color: inherit; }
    a { color: var(--accent); text-decoration: none; }
    a:hover { text-decoration: underline; }

    /* ── layout shell ───────────────────────────────────────────── */
    #app { display: grid; grid-template-columns: 320px 1fr; grid-template-rows: 56px 1fr; height: 100vh; gap: 1px; background: var(--line); }
    #header { grid-column: 1 / -1; background: var(--panel); display: flex; align-items: center; padding: 0 16px; gap: 12px; }
    #header .brand { font-weight: 700; letter-spacing: 0.02em; }
    #header .brand small { display: block; opacity: 0.55; font-weight: 400; font-size: 11px; }
    #header .stats { display: flex; gap: 14px; font-size: 12px; color: var(--muted); margin-left: 12px; }
    #header .stats b { color: var(--fg); font-weight: 600; }
    #header .right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
    .icon-btn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; }
    .icon-btn:hover { border-color: var(--accent); color: var(--accent); }

    #leftrail { background: var(--panel); display: flex; flex-direction: column; min-height: 0; }
    #leftrail .filter-bar { padding: 10px; border-bottom: 1px solid var(--line); display: flex; gap: 6px; flex-wrap: wrap; }
    #leftrail .filter-bar input { flex: 1 1 100%; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; padding: 7px 10px; color: var(--fg); outline: none; }
    #leftrail .filter-bar input:focus { border-color: var(--accent); }
    #leftrail .chip { padding: 4px 9px; border-radius: 999px; font-size: 11px; border: 1px solid var(--line); background: var(--panel-2); cursor: pointer; user-select: none; }
    #leftrail .chip.active { border-color: var(--accent); color: var(--accent); }
    #leftrail .chip .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
    #leftrail .list { flex: 1; min-height: 0; overflow-y: auto; }
    #leftrail .row { padding: 9px 12px; border-bottom: 1px solid var(--line); cursor: pointer; display: grid; grid-template-columns: 12px 1fr auto; gap: 8px; align-items: center; }
    #leftrail .row:hover { background: var(--panel-2); }
    #leftrail .row.selected { background: linear-gradient(90deg, rgba(255,176,0,0.16), transparent); border-left: 2px solid var(--accent); padding-left: 10px; }
    #leftrail .row .health-dot { width: 10px; height: 10px; border-radius: 50%; }
    .health-hot  { background: var(--hot);  box-shadow: 0 0 8px var(--hot); }
    .health-warm { background: var(--warm); }
    .health-cool { background: var(--cool); }
    .health-cold { background: var(--cold); }
    #leftrail .row .name { font-weight: 600; font-size: 13px; }
    #leftrail .row .meta { font-size: 11px; color: var(--muted); }
    #leftrail .row .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; border: 1px solid; line-height: 1.4; }
    .verdict-keep    { color: #2ecc71; border-color: rgba(46,204,113,0.4); }
    .verdict-kill    { color: #ff6b5a; border-color: rgba(255,107,90,0.4); }
    .verdict-archive { color: var(--muted); border-color: var(--line); }
    .verdict-pivot   { color: #4ea1ff; border-color: rgba(78,161,255,0.4); }
    .verdict-none { display: none; }
    .priority-pill { font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--accent); color: #1a1100; font-weight: 700; }

    /* ── main pane ─────────────────────────────────────────────── */
    #main { background: var(--bg); display: grid; grid-template-rows: auto auto 1fr auto; min-height: 0; overflow: hidden; }
    #project-header { padding: 16px 24px; background: var(--panel); border-bottom: 1px solid var(--line); }
    #project-header h1 { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
    #project-header .sub { color: var(--muted); font-size: 12px; display: flex; gap: 16px; flex-wrap: wrap; }
    #project-header .sub b { color: var(--fg); }
    #project-header .last-subj { margin-top: 6px; font-size: 12px; color: var(--muted); font-style: italic; }
    #project-header .readme { margin-top: 8px; padding: 8px 10px; background: var(--panel-2); border-radius: 6px; font-size: 12px; line-height: 1.45; max-height: 110px; overflow-y: auto; white-space: pre-wrap; }

    #verdict-bar { padding: 10px 24px; background: var(--panel); border-bottom: 1px solid var(--line); display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
    .verdict-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--panel-2); cursor: pointer; font-size: 12px; }
    .verdict-btn:hover { border-color: var(--accent); }
    .verdict-btn.active.keep    { background: rgba(46,204,113,0.18); border-color: rgba(46,204,113,0.4); color: #2ecc71; }
    .verdict-btn.active.kill    { background: rgba(255,107,90,0.18); border-color: rgba(255,107,90,0.4); color: #ff6b5a; }
    .verdict-btn.active.archive { background: var(--panel-2); border-color: var(--accent); }
    .verdict-btn.active.pivot   { background: rgba(78,161,255,0.18); border-color: rgba(78,161,255,0.4); color: #4ea1ff; }
    #priority-slider { display: flex; align-items: center; gap: 8px; margin-left: auto; font-size: 12px; color: var(--muted); }
    #priority-slider input { width: 110px; }

    /* ── pros/cons split ───────────────────────────────────────── */
    #pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; min-height: 0; overflow: hidden; }
    .pane { background: var(--panel); display: flex; flex-direction: column; min-height: 0; }
    .pane.cons { background: linear-gradient(180deg, var(--con-bg), var(--panel) 60%); }
    .pane.pros { background: linear-gradient(180deg, var(--pro-bg), var(--panel) 60%); }
    .pane h2 { margin: 0; padding: 12px 16px 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
    .pane.cons h2 { color: var(--con-fg); }
    .pane.pros h2 { color: var(--pro-fg); }
    .pane h2 small { font-weight: 400; opacity: 0.7; letter-spacing: 0; text-transform: none; margin-left: 6px; font-size: 11px; }
    .pane textarea { flex: 1; min-height: 0; resize: none; background: transparent; color: var(--fg); border: none; outline: none; padding: 8px 16px 16px; font-family: ui-monospace, SF Mono, Menlo, monospace; font-size: 12.5px; line-height: 1.55; }
    .pane .pane-toolbar { padding: 4px 16px; border-top: 1px solid var(--line); display: flex; gap: 6px; font-size: 11px; color: var(--muted); }
    .pane .pane-toolbar button { background: transparent; border: 1px solid var(--line); border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 11px; }
    .pane .pane-toolbar button:hover { border-color: var(--accent); color: var(--accent); }

    /* ── chat row at bottom ────────────────────────────────────── */
    #chat { background: var(--panel); border-top: 1px solid var(--line); display: flex; flex-direction: column; min-height: 220px; max-height: 380px; }
    #chat .log { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 16px; font-size: 12.5px; }
    #chat .log .turn { margin-bottom: 10px; }
    #chat .log .turn .role { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.5; margin-bottom: 2px; }
    #chat .log .turn.user .role { color: var(--accent); }
    #chat .log .turn.assistant .role { color: var(--cool); }
    #chat .log .turn pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: ui-monospace, SF Mono, Menlo, monospace; font-size: 12.5px; }
    #chat .input-row { display: flex; gap: 8px; padding: 8px 12px; border-top: 1px solid var(--line); align-items: flex-end; }
    #chat .input-row textarea { flex: 1; background: var(--panel-2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; outline: none; resize: none; min-height: 38px; max-height: 140px; font-family: inherit; }
    #chat .input-row textarea:focus { border-color: var(--accent); }
    #chat .input-row button { background: var(--accent); color: #1a1100; border: none; border-radius: 6px; padding: 9px 14px; font-weight: 700; cursor: pointer; }
    #chat .input-row button:disabled { opacity: 0.5; cursor: wait; }
    #chat .quick-row { display: flex; gap: 6px; padding: 6px 12px 0; flex-wrap: wrap; align-items: center; }
    #chat .quick { font-size: 11px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--muted); cursor: pointer; }
    #chat .quick:hover { border-color: var(--accent); color: var(--accent); }
    #chat .quick kbd { background: var(--kbd-bg); padding: 0 4px; border-radius: 3px; font-size: 10px; margin-left: 4px; }
    #chat .actions-row { padding: 6px 12px; display: flex; gap: 6px; flex-wrap: wrap; border-top: 1px solid var(--line); }
    #chat .action { font-size: 11px; padding: 4px 9px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); cursor: pointer; }
    #chat .action:hover { border-color: var(--accent); color: var(--accent); }
    #chat .action.danger:hover { border-color: var(--con-fg); color: var(--con-fg); }

    .empty-state { padding: 60px 30px; text-align: center; color: var(--muted); }
    .empty-state h2 { color: var(--fg); font-weight: 600; }
  </style>
</head>
<body>
  <div id="app">
    <div id="header">
      <div class="brand">build debrief<small id="header-sub">/ Steve's project board</small></div>
      <div class="stats" id="stats"></div>
      <div class="right">
        <button class="icon-btn" id="refresh-btn" title="Refresh inventory (re-walk ~/Projects/)">↻</button>
        <button class="icon-btn" id="theme-btn" title="Toggle theme">◐</button>
      </div>
    </div>

    <aside id="leftrail">
      <div class="filter-bar">
        <input id="search" type="search" placeholder="filter — name, readme, last commit subject…" autocomplete="off" />
        <span class="chip active" data-filter="all" id="chip-all">all</span>
        <span class="chip" data-filter="hot"><span class="dot health-hot"></span>hot</span>
        <span class="chip" data-filter="warm"><span class="dot health-warm"></span>warm</span>
        <span class="chip" data-filter="cool"><span class="dot health-cool"></span>cool</span>
        <span class="chip" data-filter="cold"><span class="dot health-cold"></span>cold</span>
        <span class="chip" data-filter="undecided">undecided</span>
        <span class="chip" data-filter="kill">kill</span>
      </div>
      <div class="list" id="project-list"></div>
    </aside>

    <main id="main">
      <div id="empty-pane" class="empty-state">
        <h2>Pick a project on the left</h2>
        <p>or filter by health / verdict — kill what you don't believe in, boost what you do.</p>
      </div>
    </main>
  </div>

  <template id="project-detail">
    <div id="project-header">
      <h1 data-bind="name"></h1>
      <div class="sub">
        <span><b data-bind="commit_count">0</b> commits</span>
        <span><b data-bind="size_mb">0</b> MB</span>
        <span>last: <b data-bind="last_at_pretty"></b></span>
        <span data-bind="path" style="font-family: ui-monospace, SF Mono, Menlo, monospace; font-size: 11px;"></span>
      </div>
      <div class="last-subj" data-bind="last_subject"></div>
      <div class="readme" data-bind="readme_or_pkg"></div>
    </div>

    <div id="verdict-bar">
      <button class="verdict-btn keep"    data-verdict="keep">✓ keep</button>
      <button class="verdict-btn pivot"   data-verdict="pivot">⤴ pivot</button>
      <button class="verdict-btn archive" data-verdict="archive">📦 archive</button>
      <button class="verdict-btn kill"    data-verdict="kill">✕ kill</button>
      <button class="verdict-btn"         data-verdict="">clear</button>
      <div id="priority-slider">
        priority <output id="priority-val">0</output> /10
        <input type="range" min="0" max="10" step="1" id="priority-input" />
      </div>
    </div>

    <div id="pros-cons">
      <section class="pane cons">
        <h2>why this is a bad idea <small>cons / risks</small></h2>
        <textarea id="cons-text" placeholder="• kills weekend bandwidth&#10;• violates standing rule X&#10;• overlaps with project Y…"></textarea>
        <div class="pane-toolbar">
          <button data-auto="cons">↯ auto-write</button>
          <span style="margin-left:auto;">⌘S to save</span>
        </div>
      </section>
      <section class="pane pros">
        <h2>why this is a good idea <small>pros / wins</small></h2>
        <textarea id="pros-text" placeholder="• unique angle on…&#10;• fits standing rule&#10;• revenue-shaped…"></textarea>
        <div class="pane-toolbar">
          <button data-auto="pros">↯ auto-write</button>
          <button data-auto="both">↯ auto both</button>
        </div>
      </section>
    </div>

    <div id="chat">
      <div class="quick-row">
        <button class="quick" data-prompt="Roast this project. Be honest. Where am I kidding myself?">🔥 roast</button>
        <button class="quick" data-prompt="Steel-man both sides. Give me 3 reasons to keep this and 3 reasons to kill it.">⚖ steel-man</button>
        <button class="quick" data-prompt="If I killed this today, what concretely breaks? Walk the dependency graph.">☠ kill-impact</button>
        <button class="quick" data-prompt="What's the smallest thing I could ship in the next 4 hours that would make this matter?">🎯 ship-now</button>
        <button class="quick" data-prompt="Find the single highest-leverage next commit. One sentence.">🪛 next-commit</button>
        <button class="quick" data-prompt="Summarize this project in 2 sentences for my pitch deck.">📣 pitch-2</button>
      </div>
      <div class="log" id="chat-log"></div>
      <div class="actions-row">
        <button class="action" data-action="open-iterm">⌨ open in iTerm</button>
        <button class="action" data-action="open-claude">🤖 open Claude tab</button>
        <button class="action" data-action="reveal">📂 reveal in Finder</button>
        <button class="action" data-action="abramstasks">🌀 fan-out tasks…</button>
        <button class="action" data-action="gitify">⎇ gitify</button>
      </div>
      <div class="input-row">
        <textarea id="chat-input" placeholder="ask about this project — slash commands work (/kill /pivot /run /ship). ⌘↵ to send…" rows="1"></textarea>
        <button id="chat-send">send ⌘↵</button>
      </div>
    </div>
  </template>

  <script>
    // ── state ──────────────────────────────────────────────────
    let allProjects = [];
    let filtered = [];
    let selectedId = null;
    let activeFilter = 'all';
    let searchTerm = '';
    let saveDebounce = null;

    const esc = (s) => (s == null ? '' : String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c])));
    const fmtDate = (s) => { try { return new Date(s).toISOString().slice(0,10); } catch { return '—'; } };

    // ── boot ───────────────────────────────────────────────────
    async function load() {
      const r = await fetch('/api/projects');
      const j = await r.json();
      allProjects = j.projects || [];
      document.getElementById('stats').innerHTML =
        `<span><b>${j.total}</b> builds</span>` +
        `<span><b>${allProjects.filter(p=>p.health==='hot').length}</b> hot</span>` +
        `<span><b>${allProjects.filter(p=>p.verdict==='keep').length}</b> keep</span>` +
        `<span><b>${allProjects.filter(p=>p.verdict==='kill').length}</b> kill</span>` +
        `<span><b>${allProjects.filter(p=>!p.verdict).length}</b> undecided</span>`;
      renderList();
    }

    function renderList() {
      const term = searchTerm.toLowerCase();
      filtered = allProjects.filter(p => {
        if (activeFilter !== 'all') {
          if (activeFilter === 'undecided') { if (p.verdict) return false; }
          else if (['hot','warm','cool','cold'].includes(activeFilter)) { if (p.health !== activeFilter) return false; }
          else if (p.verdict !== activeFilter) return false;
        }
        if (term) {
          const haystack = (p.name + ' ' + (p.readme_snippet||'') + ' ' + (p.last_subject||'') + ' ' + (p.pkg_description||'')).toLowerCase();
          if (!haystack.includes(term)) return false;
        }
        return true;
      });

      const list = document.getElementById('project-list');
      list.innerHTML = filtered.map(p => {
        const verdictBadge = p.verdict ? `<span class="badge verdict-${p.verdict}">${p.verdict}</span>` : '';
        const prioBadge = p.priority > 0 ? `<span class="priority-pill">P${p.priority}</span>` : '';
        return `<div class="row${p.id===selectedId?' selected':''}" data-id="${esc(p.id)}">
          <span class="health-dot health-${esc(p.health)}" title="${p.health} — ${p.days_since}d since last commit"></span>
          <div>
            <div class="name">${esc(p.name)}</div>
            <div class="meta">${fmtDate(p.first_at)} → ${fmtDate(p.last_at)} · ${p.commit_count} commits · ${p.size_mb}MB</div>
          </div>
          <div style="display:flex;flex-direction:column;gap:3px;align-items:flex-end;">${prioBadge}${verdictBadge}</div>
        </div>`;
      }).join('') || `<div class="empty-state" style="padding:30px 20px;">no matches</div>`;

      list.querySelectorAll('.row').forEach(el => {
        el.addEventListener('click', () => select(el.dataset.id));
      });
    }

    // ── select + render detail ─────────────────────────────────
    function select(id) {
      selectedId = id;
      const p = allProjects.find(x => x.id === id);
      if (!p) return;
      const main = document.getElementById('main');
      const tpl = document.getElementById('project-detail');
      main.innerHTML = '';
      main.appendChild(tpl.content.cloneNode(true));
      // bind
      const setBind = (key, val) => main.querySelectorAll(`[data-bind="${key}"]`).forEach(n => n.textContent = val);
      setBind('name', p.name);
      setBind('commit_count', p.commit_count);
      setBind('size_mb', p.size_mb);
      setBind('last_at_pretty', `${fmtDate(p.last_at)} (${p.days_since}d)`);
      setBind('path', p.path);
      setBind('last_subject', p.last_subject ? '— ' + p.last_subject : '');
      setBind('readme_or_pkg', p.readme_snippet || p.pkg_description || '(no README; no package description)');
      main.querySelector('#cons-text').value = p.cons || '';
      main.querySelector('#pros-text').value = p.pros || '';
      main.querySelector('#priority-input').value = p.priority || 0;
      main.querySelector('#priority-val').textContent = p.priority || 0;

      // verdict highlight
      main.querySelectorAll('.verdict-btn').forEach(b => {
        b.classList.toggle('active', b.dataset.verdict === (p.verdict || ''));
      });

      wireDetail(p);
      renderChat(p);
      // refresh sidebar selection
      document.querySelectorAll('#project-list .row').forEach(r => r.classList.toggle('selected', r.dataset.id === id));
    }

    function renderChat(p) {
      const log = document.getElementById('chat-log');
      log.innerHTML = (p.chat || []).map(t => `<div class="turn ${t.role}">
        <div class="role">${t.role === 'user' ? 'you' : 'claude'}${t.error?' · error':''}</div>
        <pre>${esc(t.content)}</pre>
      </div>`).join('') || '<div style="color:var(--muted);font-size:12px;">no chat yet — try a quick-prompt above ↑</div>';
      log.scrollTop = log.scrollHeight;
    }

    function wireDetail(p) {
      const main = document.getElementById('main');

      // verdict
      main.querySelectorAll('.verdict-btn').forEach(b => {
        b.addEventListener('click', async () => {
          const v = b.dataset.verdict || null;
          await fetch(`/api/project/${p.id}/verdict`, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ verdict: v }) });
          p.verdict = v;
          main.querySelectorAll('.verdict-btn').forEach(x => x.classList.toggle('active', x.dataset.verdict === (v || '')));
          renderList();
          load(); // refresh stats
        });
      });

      // priority
      const prioIn = main.querySelector('#priority-input');
      const prioOut = main.querySelector('#priority-val');
      prioIn.addEventListener('input', () => { prioOut.textContent = prioIn.value; });
      prioIn.addEventListener('change', async () => {
        const n = parseInt(prioIn.value, 10);
        await fetch(`/api/project/${p.id}/priority`, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ priority: n }) });
        p.priority = n;
        renderList();
      });

      // pros / cons textarea autosave
      const cons = main.querySelector('#cons-text');
      const pros = main.querySelector('#pros-text');
      const saveNotes = () => {
        clearTimeout(saveDebounce);
        saveDebounce = setTimeout(async () => {
          await fetch(`/api/project/${p.id}/notes`, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ pros: pros.value, cons: cons.value }) });
          p.pros = pros.value; p.cons = cons.value;
        }, 500);
      };
      cons.addEventListener('input', saveNotes);
      pros.addEventListener('input', saveNotes);

      // ⌘S
      [cons, pros].forEach(t => t.addEventListener('keydown', (e) => {
        if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); saveNotes(); }
      }));

      // auto-write pros/cons
      main.querySelectorAll('[data-auto]').forEach(b => {
        b.addEventListener('click', async () => {
          const which = b.dataset.auto;
          b.disabled = true; b.textContent = '↯ thinking…';
          try {
            const r = await fetch(`/api/project/${p.id}/auto-pros-cons`, { method: 'POST' });
            const j = await r.json();
            if (j.ok) {
              if (which === 'pros' || which === 'both') pros.value = j.pros;
              if (which === 'cons' || which === 'both') cons.value = j.cons;
              p.pros = j.pros; p.cons = j.cons;
            } else {
              alert('LLM unreachable. Open a Claude tab from the chat actions instead.');
            }
          } finally { b.disabled = false; b.textContent = b.dataset.auto === 'both' ? '↯ auto both' : '↯ auto-write'; }
        });
      });

      // chat
      const chatInput = main.querySelector('#chat-input');
      const chatSend = main.querySelector('#chat-send');
      const sendChat = async (msg) => {
        const text = (msg ?? chatInput.value).trim();
        if (!text) return;
        chatInput.value = '';
        chatSend.disabled = true; chatSend.textContent = '…';
        // optimistic add
        p.chat = p.chat || [];
        p.chat.push({ role: 'user', content: text, at: new Date().toISOString() });
        p.chat.push({ role: 'assistant', content: '…thinking…', at: new Date().toISOString(), pending: true });
        renderChat(p);
        try {
          const r = await fetch(`/api/project/${p.id}/chat`, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ message: text }) });
          const j = await r.json();
          if (j.chat) p.chat = j.chat;
        } catch { p.chat[p.chat.length-1] = { role: 'assistant', content: '(network error)', error: true }; }
        chatSend.disabled = false; chatSend.textContent = 'send ⌘↵';
        renderChat(p);
      };
      chatSend.addEventListener('click', () => sendChat());
      chatInput.addEventListener('keydown', (e) => {
        if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); sendChat(); }
      });
      main.querySelectorAll('.quick').forEach(b => {
        b.addEventListener('click', () => sendChat(b.dataset.prompt));
      });

      // actions
      main.querySelectorAll('.action').forEach(b => {
        b.addEventListener('click', async () => {
          const action = b.dataset.action;
          const args = {};
          if (action === 'abramstasks') {
            const txt = prompt('one task per line:');
            if (!txt) return;
            args.tasks = txt.split('\n').filter(Boolean);
          }
          if (action === 'open-claude') {
            const seed = prompt('opening prompt for the Claude tab (blank = default):', '') || '';
            if (seed) args.prompt = seed;
          }
          b.disabled = true; const orig = b.textContent; b.textContent = '…';
          try {
            const r = await fetch(`/api/project/${p.id}/action`, { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify({ action, args }) });
            const j = await r.json();
            if (!j.ok) alert(j.reason || j.error || 'action failed');
          } catch (e) { alert(String(e)); }
          finally { b.disabled = false; b.textContent = orig; }
        });
      });
    }

    // ── filter chips + search + theme + refresh ───────────────
    document.querySelectorAll('.chip').forEach(c => {
      c.addEventListener('click', () => {
        document.querySelectorAll('.chip').forEach(x => x.classList.toggle('active', x === c));
        activeFilter = c.dataset.filter;
        renderList();
      });
    });
    document.getElementById('search').addEventListener('input', (e) => { searchTerm = e.target.value; renderList(); });
    document.getElementById('theme-btn').addEventListener('click', () => {
      const cur = document.documentElement.getAttribute('data-theme');
      const next = cur === 'dark' ? 'light' : 'dark';
      document.documentElement.setAttribute('data-theme', next);
      try { localStorage.setItem('debrief-theme', next); } catch {}
    });
    document.getElementById('refresh-btn').addEventListener('click', async (e) => {
      e.target.textContent = '⟳';
      const r = await fetch('/api/refresh-inventory', { method: 'POST' });
      const j = await r.json();
      e.target.textContent = '↻';
      if (!j.ok) alert(`refresh failed (code ${j.code})`);
      await load();
      if (selectedId) {
        const r2 = await fetch(`/api/project/${selectedId}`);
        if (r2.ok) {
          // refresh details
          select(selectedId);
        }
      }
    });

    load();
  </script>
</body>
</html>