← 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 & 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 →