← 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 • violates standing rule X • 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… • fits standing rule • 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 => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[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>