@@ -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, '&' )
1334+ . replace ( / < / g, '<' )
1335+ . replace ( / > / g, '>' ) ;
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