[object Object]

← back to Agent Cabinet

Activity viewer: live search box — filters the feed AND scheduled/continuous jobs by title/detail/type/label/cadence; chip counts track the query; clear button + 'no matches' state; persists across auto-refresh

c791b735527775902b3ccc49a9ef75f7fede101b · 2026-06-16 18:17:05 -0700 · SteveStudio2

Files touched

Diff

commit c791b735527775902b3ccc49a9ef75f7fede101b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue Jun 16 18:17:05 2026 -0700

    Activity viewer: live search box — filters the feed AND scheduled/continuous jobs by title/detail/type/label/cadence; chip counts track the query; clear button + 'no matches' state; persists across auto-refresh
---
 activity.html | 31 +++++++++++++++++++++++--------
 1 file changed, 23 insertions(+), 8 deletions(-)

diff --git a/activity.html b/activity.html
index f5ff26f..8bb3040 100644
--- a/activity.html
+++ b/activity.html
@@ -16,6 +16,11 @@
   .chip.on{color:#fff}
   .chip .dot{width:9px;height:9px;border-radius:50%}
   .chip .n{font:11px ui-monospace,monospace;opacity:.8}
+  .searchrow{margin-top:11px;display:flex;align-items:center;gap:10px}
+  #search{flex:1;max-width:440px;background:#0a0e16;border:1px solid #2b3650;color:#e7ecf3;border-radius:8px;padding:8px 12px;font-size:13px}
+  #search:focus{outline:none;border-color:#34f4ce}
+  #search-x{color:var(--dim);font-size:12px;cursor:pointer;display:none}
+  #search-x.show{display:inline}
   main{max-width:1340px;margin:0 auto;padding:18px 28px 70px}
   .cols{display:grid;grid-template-columns:var(--col-w,380px) 14px minmax(0,1fr);align-items:start}
   .col-up{position:sticky;top:132px;max-height:calc(100vh - 150px);overflow:auto;padding-right:6px}
@@ -67,6 +72,7 @@
   <h1>What it's doing <small>live activity timeline — agents, canaries, watch ticks, commits, spend</small></h1>
   <div class="meta"><span class="pulse"></span><span id="meta">loading…</span> · auto-refreshes every 30s · <a href="/pyramid">← cabinet pyramid</a></div>
   <div class="chips" id="chips"></div>
+  <div class="searchrow"><input id="search" type="search" placeholder="🔎 filter events &amp; jobs — title, vendor, repo, skill…" autocomplete="off"><span id="search-x" onclick="clearSearch()">✕ clear</span></div>
 </header>
 <div id="det"><div class="det-head"><span id="det-title">—</span><button class="det-x" onclick="closeDet()">✕</button></div><div id="det-body"></div></div>
 <main class="cols">
@@ -83,16 +89,18 @@ const TYPES={
   tick:{label:'Watch ticks', col:'#60a5fa'},
 };
 const VCOL={FAIL:'#ef4444',WARN:'#fbbf24',PASS:'#34d399',NA:'#6b7280'};
-let DATA=[], OFF_FILTER=new Set();   // types hidden
+let DATA=[], OFF_FILTER=new Set(), QUERY='';   // types hidden + search query
+function matchQ(e){ return !QUERY || ((e.title||'')+' '+(e.detail||'')+' '+(e.type||'')).toLowerCase().includes(QUERY); }
 function colorFor(e){ if(e.type==='canary') return VCOL[e.verdict]||TYPES.canary.col; return (TYPES[e.type]||{}).col||'#2b3650'; }
 function fmtTime(iso){ try{ return new Date(iso).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'}); }catch(e){ return '--:--'; } }
 function fmtDay(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{weekday:'short',month:'short',day:'numeric',year:'numeric'}); }catch(e){ return iso; } }
 function dayKey(iso){ try{ return new Date(iso).toISOString().slice(0,10); }catch(e){ return iso.slice(0,10); } }
 
 function renderChips(){
-  const counts={}; DATA.forEach(e=>counts[e.type]=(counts[e.type]||0)+1);
+  const set=DATA.filter(matchQ);
+  const counts={}; set.forEach(e=>counts[e.type]=(counts[e.type]||0)+1);
   const c=document.getElementById('chips');
-  c.innerHTML='<span class="chip '+(OFF_FILTER.size?'':'on')+'" data-t="__all"><b>All</b> <span class="n">'+DATA.length+'</span></span>'+
+  c.innerHTML='<span class="chip '+(OFF_FILTER.size?'':'on')+'" data-t="__all"><b>All</b> <span class="n">'+set.length+'</span></span>'+
     Object.keys(TYPES).map(t=>`<span class="chip ${OFF_FILTER.has(t)?'':'on'}" data-t="${t}"><span class="dot" style="background:${TYPES[t].col}"></span>${TYPES[t].label} <span class="n">${counts[t]||0}</span></span>`).join('');
   c.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{
     const t=ch.dataset.t;
@@ -101,9 +109,9 @@ function renderChips(){
   });
 }
 function renderFeed(){
-  const shown=DATA.filter(e=>!OFF_FILTER.has(e.type));
+  const shown=DATA.filter(e=>!OFF_FILTER.has(e.type) && matchQ(e));
   const feed=document.getElementById('feed');
-  if(!shown.length){ feed.innerHTML='<div class="empty">No events for this filter.</div>'; return; }
+  if(!shown.length){ feed.innerHTML='<div class="empty">'+(QUERY?'No matches for “'+esc(QUERY)+'”.':'No events for this filter.')+'</div>'; return; }
   let html='', curDay=null;
   shown.forEach(e=>{
     const dk=dayKey(e.ts);
@@ -125,10 +133,14 @@ async function load(){
 }
 function rel(ms){ const d=ms-Date.now(); if(d<=0) return 'now'; const m=Math.round(d/60000); if(m<60) return 'in '+m+'m'; const h=Math.floor(m/60); return 'in '+h+'h '+String(m%60).padStart(2,'0')+'m'; }
 function fmtAbs(iso){ try{ return new Date(iso).toLocaleString(undefined,{weekday:'short',hour:'2-digit',minute:'2-digit'}); }catch(e){ return iso; } }
-let SHOW_ALL_UP=false;
+let SHOW_ALL_UP=false, SCHED={upcoming:[],continuous:[]};
 async function loadSchedule(){
-  let r; try{ r=await(await fetch('/api/schedule')).json(); }catch(e){ return; }
-  const up=r.upcoming||[], cont=r.continuous||[];
+  try{ const r=await(await fetch('/api/schedule')).json(); SCHED={upcoming:r.upcoming||[],continuous:r.continuous||[]}; }catch(e){}
+  renderSchedule();
+}
+function renderSchedule(){
+  let up=SCHED.upcoming, cont=SCHED.continuous;
+  if(QUERY){ up=up.filter(e=>((e.label||'')+' '+(e.cadence||'')).toLowerCase().includes(QUERY)); cont=cont.filter(c=>((c.label||'')+' '+(c.cadence||'')).toLowerCase().includes(QUERY)); }
   const n=SHOW_ALL_UP?up.length:14;
   let h='<div class="up-head">⏭ Upcoming <span class="s">— '+up.length+' scheduled jobs · next fire</span></div>';
   h+=up.slice(0,n).map(e=>`<div class="up-row" data-label="${escA(e.label)}"><span class="rel">${rel(Date.parse(e.next_ts))}</span><span class="abs">${fmtAbs(e.next_ts)}</span><span class="lab">${esc(e.label)}</span><span class="cad">${esc(e.cadence)}</span></div>`).join('');
@@ -192,6 +204,9 @@ document.addEventListener('keydown', e=>{ if(e.key==='Escape') closeDet(); });
   window.addEventListener('mouseup', ()=>{ if(!dragging) return; dragging=false; sp.classList.remove('drag'); document.body.style.userSelect=''; const w=parseInt(cols.style.getPropertyValue('--col-w'),10); if(w) localStorage.setItem(KEY,w); });
   sp.addEventListener('dblclick', ()=>{ cols.style.setProperty('--col-w','380px'); localStorage.setItem(KEY,'380'); });
 })();
+const _si=document.getElementById('search');
+_si.addEventListener('input', ()=>{ QUERY=_si.value.trim().toLowerCase(); document.getElementById('search-x').classList.toggle('show', !!QUERY); renderChips(); renderFeed(); renderSchedule(); });
+function clearSearch(){ _si.value=''; QUERY=''; document.getElementById('search-x').classList.remove('show'); _si.focus(); renderChips(); renderFeed(); renderSchedule(); }
 load(); loadSchedule(); setInterval(()=>{ load(); loadSchedule(); }, 30000);
 </script>
 </body></html>

← 9727194 Activity viewer: click any process/event for details — detai  ·  back to Agent Cabinet  ·  Activity viewer: date-range filter — from/to date pickers + c18a8e8 →