Skip to content

Commit 9e70fe1

Browse files
committed
feat: add hooks browser overlay
1 parent eba74ec commit 9e70fe1

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

client/dashboard.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class Dashboard {
122122
<button class="dashboard-topbar-btn" id="dashboard-open-telemetry-details" title="View trends and histograms">📈 Details</button>
123123
<button class="dashboard-topbar-btn" id="dashboard-open-performance" title="Per-terminal resource usage">⚙ Perf</button>
124124
<button class="dashboard-topbar-btn" id="dashboard-open-polecats" title="Manage sessions (restart/kill/logs)">🐾 Polecats</button>
125+
<button class="dashboard-topbar-btn" id="dashboard-open-hooks" title="Hook browser (automations/webhooks)">🪝 Hooks</button>
125126
<button class="dashboard-topbar-btn" id="dashboard-open-tests" title="Run tests across worktrees">🧪 Tests</button>
126127
<button class="dashboard-topbar-btn" id="dashboard-export-telemetry" title="Download telemetry CSV export">⬇ Export</button>
127128
<button class="dashboard-topbar-btn" id="dashboard-export-telemetry-json" title="Download telemetry JSON export">⬇ JSON</button>
@@ -219,6 +220,10 @@ class Dashboard {
219220
e.preventDefault();
220221
this.showPolecatOverlay().catch(() => {});
221222
});
223+
document.getElementById('dashboard-open-hooks')?.addEventListener('click', (e) => {
224+
e.preventDefault();
225+
this.showHooksOverlay().catch(() => {});
226+
});
222227
document.getElementById('dashboard-open-polecats-card')?.addEventListener('click', (e) => {
223228
e.preventDefault();
224229
this.showPolecatOverlay().catch(() => {});
@@ -1259,6 +1264,172 @@ class Dashboard {
12591264
});
12601265
}
12611266

1267+
async showHooksOverlay() {
1268+
const existing = document.getElementById('dashboard-hooks-overlay');
1269+
if (existing) {
1270+
existing.classList.remove('hidden');
1271+
return;
1272+
}
1273+
1274+
const overlay = document.createElement('div');
1275+
overlay.id = 'dashboard-hooks-overlay';
1276+
overlay.className = 'dashboard-telemetry-overlay';
1277+
overlay.innerHTML = `
1278+
<div class="dashboard-telemetry-panel" role="dialog" aria-label="Hook browser">
1279+
<div class="dashboard-telemetry-header">
1280+
<div class="dashboard-telemetry-title">Hooks — Automations</div>
1281+
<button class="dashboard-topbar-btn" id="dashboard-hooks-close" title="Close (Esc)">✕</button>
1282+
</div>
1283+
<div class="dashboard-telemetry-controls">
1284+
<div class="dashboard-telemetry-actions">
1285+
<button class="btn-secondary" type="button" id="dashboard-hooks-refresh">Refresh</button>
1286+
</div>
1287+
</div>
1288+
<div id="dashboard-hooks-body" class="dashboard-telemetry-body">Loading…</div>
1289+
</div>
1290+
`;
1291+
1292+
document.body.appendChild(overlay);
1293+
1294+
const close = () => this.hideHooksOverlay();
1295+
overlay.addEventListener('click', (e) => {
1296+
if (e.target === overlay) close();
1297+
});
1298+
overlay.querySelector('#dashboard-hooks-close')?.addEventListener('click', close);
1299+
overlay.querySelector('#dashboard-hooks-refresh')?.addEventListener('click', () => {
1300+
this.loadHooksDetails().catch(() => {});
1301+
});
1302+
1303+
const onKey = (e) => {
1304+
if (e.key !== 'Escape') return;
1305+
const el = document.getElementById('dashboard-hooks-overlay');
1306+
if (!el || el.classList.contains('hidden')) return;
1307+
close();
1308+
};
1309+
overlay._escHandler = onKey;
1310+
document.addEventListener('keydown', onKey);
1311+
1312+
await this.loadHooksDetails();
1313+
}
1314+
1315+
hideHooksOverlay() {
1316+
const overlay = document.getElementById('dashboard-hooks-overlay');
1317+
if (!overlay) return;
1318+
overlay.classList.add('hidden');
1319+
const handler = overlay._escHandler;
1320+
if (handler) {
1321+
document.removeEventListener('keydown', handler);
1322+
overlay._escHandler = null;
1323+
}
1324+
overlay.remove();
1325+
}
1326+
1327+
async loadHooksDetails() {
1328+
const bodyEl = document.getElementById('dashboard-hooks-body');
1329+
if (!bodyEl) return;
1330+
bodyEl.textContent = 'Loading…';
1331+
1332+
const escapeHtml = (value) => String(value ?? '')
1333+
.replace(/&/g, '&amp;')
1334+
.replace(/</g, '&lt;')
1335+
.replace(/>/g, '&gt;');
1336+
1337+
let automations = null;
1338+
try {
1339+
const res = await fetch('/api/process/automations');
1340+
automations = res && res.ok ? await res.json().catch(() => null) : null;
1341+
} catch {
1342+
automations = null;
1343+
}
1344+
1345+
const trelloCfg = this.orchestrator?.userSettings?.global?.ui?.tasks?.automations?.trello?.onPrMerged || {};
1346+
const enabled = trelloCfg.enabled !== false;
1347+
const pollEnabled = trelloCfg.pollEnabled !== false;
1348+
const webhookEnabled = !!trelloCfg.webhookEnabled;
1349+
const comment = trelloCfg.comment !== false;
1350+
const moveToDoneList = trelloCfg.moveToDoneList !== false;
1351+
const closeIfNoDoneList = !!trelloCfg.closeIfNoDoneList;
1352+
const pollMs = Number(trelloCfg.pollMs ?? 60000);
1353+
1354+
const prMergeCfg = automations?.prMerge || {};
1355+
const lastRunAt = automations?.lastRunAt || null;
1356+
1357+
bodyEl.innerHTML = `
1358+
<div style="display:grid; gap:14px;">
1359+
<div>
1360+
<div style="font-weight:600; margin-bottom:6px;">Trello hook (on PR merged)</div>
1361+
<div class="tasks-detail-meta" style="margin-bottom:10px; opacity:0.9;">
1362+
Stored in user settings: <code>ui.tasks.automations.trello.onPrMerged</code>
1363+
</div>
1364+
<div style="display:flex; flex-wrap:wrap; gap:10px;">
1365+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-enabled" ${enabled ? 'checked' : ''}/> enabled</label>
1366+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-poll" ${pollEnabled ? 'checked' : ''}/> poll</label>
1367+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-webhook" ${webhookEnabled ? 'checked' : ''}/> webhook</label>
1368+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-comment" ${comment ? 'checked' : ''}/> comment</label>
1369+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-move" ${moveToDoneList ? 'checked' : ''}/> move card</label>
1370+
<label style="display:flex; align-items:center; gap:8px;"><input type="checkbox" id="hooks-trello-close" ${closeIfNoDoneList ? 'checked' : ''}/> close if no done list</label>
1371+
</div>
1372+
<div style="margin-top:10px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
1373+
<span class="tasks-detail-meta">pollMs</span>
1374+
<input id="hooks-trello-pollms" type="number" min="5000" step="1000" value="${escapeHtml(String(Number.isFinite(pollMs) ? pollMs : 60000))}" style="width:140px;" />
1375+
<button class="btn-secondary" type="button" id="hooks-trello-save">Save</button>
1376+
</div>
1377+
</div>
1378+
1379+
<div>
1380+
<div style="font-weight:600; margin-bottom:6px;">PR merge automation</div>
1381+
<div class="tasks-detail-meta" style="margin-bottom:8px; opacity:0.9;">
1382+
lastRunAt: ${lastRunAt ? `<code>${escapeHtml(lastRunAt)}</code>` : '—'}
1383+
</div>
1384+
<div class="tasks-detail-meta" style="margin-bottom:10px; opacity:0.9;">
1385+
Config: pollMs <code>${escapeHtml(String(prMergeCfg?.pollMs ?? '—'))}</code> • enabled <code>${escapeHtml(String(prMergeCfg?.enabled ?? '—'))}</code>
1386+
</div>
1387+
<div style="display:flex; gap:10px; flex-wrap:wrap;">
1388+
<button class="btn-secondary" type="button" id="hooks-pr-merge-run">▶ Run once</button>
1389+
</div>
1390+
<div id="hooks-pr-merge-result" class="tasks-detail-meta" style="margin-top:10px; opacity:0.9;"></div>
1391+
</div>
1392+
</div>
1393+
`;
1394+
1395+
const update = async (path, value) => {
1396+
try {
1397+
await this.orchestrator?.updateGlobalUserSetting?.(path, value);
1398+
} catch {}
1399+
};
1400+
1401+
bodyEl.querySelector('#hooks-trello-save')?.addEventListener('click', async () => {
1402+
const next = {
1403+
enabled: !!bodyEl.querySelector('#hooks-trello-enabled')?.checked,
1404+
pollEnabled: !!bodyEl.querySelector('#hooks-trello-poll')?.checked,
1405+
webhookEnabled: !!bodyEl.querySelector('#hooks-trello-webhook')?.checked,
1406+
comment: !!bodyEl.querySelector('#hooks-trello-comment')?.checked,
1407+
moveToDoneList: !!bodyEl.querySelector('#hooks-trello-move')?.checked,
1408+
closeIfNoDoneList: !!bodyEl.querySelector('#hooks-trello-close')?.checked,
1409+
pollMs: Number(bodyEl.querySelector('#hooks-trello-pollms')?.value || 60000)
1410+
};
1411+
await update('ui.tasks.automations.trello.onPrMerged', next);
1412+
this.loadHooksDetails().catch(() => {});
1413+
});
1414+
1415+
bodyEl.querySelector('#hooks-pr-merge-run')?.addEventListener('click', async () => {
1416+
const out = bodyEl.querySelector('#hooks-pr-merge-result');
1417+
if (out) out.textContent = 'Running…';
1418+
try {
1419+
const res = await fetch('/api/process/automations/pr-merge/run', {
1420+
method: 'POST',
1421+
headers: { 'Content-Type': 'application/json' },
1422+
body: JSON.stringify({ limit: 60 })
1423+
});
1424+
const data = await res.json().catch(() => ({}));
1425+
if (!res.ok) throw new Error(data?.error || 'Failed');
1426+
if (out) out.innerHTML = `ok: <code>${escapeHtml(String(!!data.ok))}</code> • processed: <code>${escapeHtml(String(data?.processed ?? 0))}</code> • moved: <code>${escapeHtml(String(data?.moved ?? 0))}</code>`;
1427+
} catch (err) {
1428+
if (out) out.textContent = `Failed: ${String(err?.message || err)}`;
1429+
}
1430+
});
1431+
}
1432+
12621433
hidePerformanceOverlay() {
12631434
const overlay = document.getElementById('dashboard-performance-overlay');
12641435
if (!overlay) return;

0 commit comments

Comments
 (0)