[object Object]

← back to Agent Cabinet

Activity viewer: FIX day-grouping used UTC date while times render local → evening-PT events grouped under wrong/duplicate day headers; dayKey now uses local date (verified 0 mismatches, unique headers). Plus: export filtered view to CSV (respects search+type+date filters; timestamp/local/type/verdict/title/detail/ref columns, RFC-escaped)

d170113be3c12086973870ef24df483f0e9037d4 · 2026-06-16 18:27:50 -0700 · SteveStudio2

Files touched

Diff

commit d170113be3c12086973870ef24df483f0e9037d4
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue Jun 16 18:27:50 2026 -0700

    Activity viewer: FIX day-grouping used UTC date while times render local → evening-PT events grouped under wrong/duplicate day headers; dayKey now uses local date (verified 0 mismatches, unique headers). Plus: export filtered view to CSV (respects search+type+date filters; timestamp/local/type/verdict/title/detail/ref columns, RFC-escaped)
---
 activity.html | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/activity.html b/activity.html
index b3a9417..1dafd4d 100644
--- a/activity.html
+++ b/activity.html
@@ -81,6 +81,7 @@
   <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>
   <div class="daterow">📅 <input type="date" id="from"> <span>→</span> <input type="date" id="to">
     <span class="pre" onclick="preset(0,this)">Today</span><span class="pre" onclick="preset(7,this)">7d</span><span class="pre" onclick="preset(30,this)">30d</span><span class="pre on" onclick="preset(null,this)">All</span>
+    <span class="pre csvbtn" onclick="exportCsv()" title="Download the currently-filtered events as CSV">⬇ Export CSV</span>
     <span id="range-n"></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>
@@ -104,7 +105,7 @@ function inRange(ts){ const t=Date.parse(ts); if(isNaN(t)) return true; return (
 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 dayKey(iso){ try{ const d=new Date(iso); return d.getFullYear()+'-'+('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2); }catch(e){ return ''; } }
 
 function renderChips(){
   const set=DATA.filter(e=>matchQ(e)&&inRange(e.ts));
@@ -118,8 +119,21 @@ function renderChips(){
     renderChips(); renderFeed();
   });
 }
+function shownEvents(){ return DATA.filter(e=>!OFF_FILTER.has(e.type) && matchQ(e) && inRange(e.ts)); }
+function csvCell(v){ v=(v==null?'':String(v)); return /[",\n]/.test(v)?'"'+v.replace(/"/g,'""')+'"':v; }
+function exportCsv(){
+  const rows=shownEvents();
+  if(!rows.length){ document.getElementById('range-n').textContent='nothing to export'; return; }
+  const head=['timestamp_iso','datetime_local','type','verdict','title','detail','ref'];
+  const out=[head.join(',')];
+  rows.forEach(e=>{ let loc=''; try{ loc=new Date(e.ts).toLocaleString(); }catch(_){}
+    out.push([e.ts, loc, e.type, e.verdict||'', e.title||'', e.detail||'', e.repo?(e.repo+' '+e.hash):(e.skill||'')].map(csvCell).join(',')); });
+  const blob=new Blob([out.join('\n')],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob);
+  a.download='activity-'+new Date().toISOString().replace(/[:T]/g,'-').slice(0,16)+'.csv'; a.click();
+  document.getElementById('range-n').textContent='exported '+rows.length+' rows';
+}
 function renderFeed(){
-  const shown=DATA.filter(e=>!OFF_FILTER.has(e.type) && matchQ(e) && inRange(e.ts));
+  const shown=shownEvents();
   const feed=document.getElementById('feed');
   if(!shown.length){ feed.innerHTML='<div class="empty">'+(QUERY||FROM||TO?'No matches for the current filters.':'No events for this filter.')+'</div>'; return; }
   let html='', curDay=null;

← c18a8e8 Activity viewer: date-range filter — from/to date pickers +  ·  back to Agent Cabinet  ·  activity /api/schedule: read real launchd state (loaded+disa 5b3ec47 →