[object Object]

← back to Zendesk Chat Analyzer

chore: lint + refactor + escape live-feed HTML (XSS guard) + v1.0.0 (session close)

307ab80a20d3dec90c7e8df585a4b686ac6794d4 · 2026-08-12 08:09:15 -0700 · Steve

Files touched

Diff

commit 307ab80a20d3dec90c7e8df585a4b686ac6794d4
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 08:09:15 2026 -0700

    chore: lint + refactor + escape live-feed HTML (XSS guard) + v1.0.0 (session close)
---
 VERSION                            |   1 +
 __pycache__/pull.cpython-314.pyc   | Bin 0 -> 12592 bytes
 __pycache__/recent.cpython-314.pyc | Bin 0 -> 7046 bytes
 public/app.js                      |  27 +++++++++++++++------------
 server.js                          |   4 ++--
 5 files changed, 18 insertions(+), 14 deletions(-)

diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/__pycache__/pull.cpython-314.pyc b/__pycache__/pull.cpython-314.pyc
new file mode 100644
index 0000000..447c185
Binary files /dev/null and b/__pycache__/pull.cpython-314.pyc differ
diff --git a/__pycache__/recent.cpython-314.pyc b/__pycache__/recent.cpython-314.pyc
new file mode 100644
index 0000000..168313c
Binary files /dev/null and b/__pycache__/recent.cpython-314.pyc differ
diff --git a/public/app.js b/public/app.js
index 5138d93..2ffed88 100644
--- a/public/app.js
+++ b/public/app.js
@@ -199,6 +199,7 @@ const COLDEFS=[
 const CMAP=Object.fromEntries(COLDEFS.map(d=>[d.k,d]));
 const DEF_ORDER=COLDEFS.map(d=>d.k);
 const DEF_HIDE=['region','browser','duration','search_terms','ticket_id','msg']; // sensible tight default
+const TABLE_ROW_CAP = 500;
 let colOrder, colHidden;
 function loadCols(){
   try{ const s=JSON.parse(localStorage.getItem('zca_cols')||'null'); colOrder=(s&&s.order||DEF_ORDER).filter(k=>CMAP[k]); DEF_ORDER.forEach(k=>{if(!colOrder.includes(k))colOrder.push(k);}); colHidden=new Set(s&&s.hidden||DEF_HIDE); }
@@ -209,17 +210,22 @@ function saveCols(){ localStorage.setItem('zca_cols',JSON.stringify({order:colOr
 const visCols=()=>colOrder.filter(k=>!colHidden.has(k));
 window.setF=(dim,v)=>{ FILTER[dim]=v; renderAll(); };
 
-function table(){
+/** Returns filtered, searched, sorted rows — shared by table() and exportCsv. */
+function filteredSortedRows(){
   let r=rows().slice();
   const q=document.getElementById('search').value.toLowerCase().split(/\s+/).filter(Boolean);
   if(q.length) r=r.filter(c=>{ const blob=COLDEFS.map(d=>d.p(c)).join(' ').toLowerCase(); return q.every(w=>blob.includes(w)); });
   const sd=CMAP[sortKey]||CMAP.ts;
   r.sort((a,b)=>{ let x=sd.p(a),y=sd.p(b); if(typeof x==='string')x=x.toLowerCase(); if(typeof y==='string')y=y.toLowerCase(); return (x>y?1:x<y?-1:0)*sortDir; });
+  return r;
+}
+function table(){
+  const r=filteredSortedRows();
   const cols=visCols();
   document.querySelector('#tbl thead').innerHTML='<tr>'+cols.map(k=>{ const d=CMAP[k];
     return `<th class="drag" draggable="true" data-k="${k}" title="drag to reorder / click to sort">`+
       `<span class="grip">⋮⋮</span>${d.l}${sortKey===k?(sortDir>0?' ▲':' ▼'):''}</th>`; }).join('')+'</tr>';
-  document.querySelector('#tbl tbody').innerHTML=r.slice(0,500).map(c=>'<tr>'+cols.map(k=>{
+  document.querySelector('#tbl tbody').innerHTML=r.slice(0,TABLE_ROW_CAP).map(c=>'<tr>'+cols.map(k=>{
     const d=CMAP[k]; const v=d.g(c); const t=String(d.p(c)).replace(/"/g,'');
     return `<td title="${t}">${(v===''||v==null)?'<span class=mut>—</span>':v}</td>`;}).join('')+'</tr>').join('');
   document.getElementById('tableCount').textContent=`${r.length} rows · ${cols.length}/${COLDEFS.length} cols`;
@@ -248,11 +254,7 @@ document.addEventListener('click',()=>document.getElementById('colPop').classLis
 document.getElementById('colPop').onclick=e=>e.stopPropagation();
 document.getElementById('resetCols').onclick=()=>{ colOrder=DEF_ORDER.slice(); colHidden=new Set(DEF_HIDE); sortKey='ts';sortDir=-1; localStorage.removeItem('zca_cols');localStorage.removeItem('zca_sort'); table(); };
 document.getElementById('exportCsv').onclick=()=>{
-  let r=rows().slice();
-  const q=document.getElementById('search').value.toLowerCase().split(/\s+/).filter(Boolean);
-  if(q.length) r=r.filter(c=>{ const blob=COLDEFS.map(d=>d.p(c)).join(' ').toLowerCase(); return q.every(w=>blob.includes(w)); });
-  const sd=CMAP[sortKey]||CMAP.ts;
-  r.sort((a,b)=>{ let x=sd.p(a),y=sd.p(b); if(typeof x==='string')x=x.toLowerCase(); if(typeof y==='string')y=y.toLowerCase(); return (x>y?1:x<y?-1:0)*sortDir; });
+  const r=filteredSortedRows(); // export honors active search + sort, no row cap
   const cols=visCols(), esc=v=>{ v=String(v==null?'':v); return /[",\n]/.test(v)?'"'+v.replace(/"/g,'""')+'"':v; };
   const csv=[cols.map(k=>esc(CMAP[k].l)).join(',')].concat(r.map(c=>cols.map(k=>esc(CMAP[k].p(c))).join(','))).join('\r\n');
   const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
@@ -270,6 +272,7 @@ document.getElementById('refresh').onclick=function(){
 
 /* ---------- Live recent-activity feed ---------- */
 const flag = cc => (cc && cc.length===2 && /^[A-Z]{2}$/.test(cc)) ? String.fromCodePoint(...[...cc].map(c=>0x1F1E6+c.charCodeAt(0)-65)) : '🌐';
+const esc = s => (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 function ago(ts, now){ try{ const t=Math.floor(new Date(ts).getTime()/1000); const s=now-t; if(s<60)return 'just now'; if(s<3600)return Math.floor(s/60)+'m ago'; if(s<86400)return Math.floor(s/3600)+'h ago'; return Math.floor(s/86400)+'d ago'; }catch(e){ return ''; } }
 let livePrevIds = new Set(), liveFirst = true;
 function pollLive(){
@@ -286,11 +289,11 @@ function pollLive(){
       return `<div class="fitem${c.missed?' miss':''}${fresh?' fresh':''}">
         <div class="ago">${ago(c.ts,now)}</div>
         <div style="flex:1;min-width:0">
-          <div><span class="who">${flag(c.country_code)} ${c.city||c.country}</span>
-            <span class="loc"> · ${c.country} · ${c.platform||''} · ${c.type==='offline_msg'?'offline msg':'chat'}</span>
-            ${c.missed?'<span class="badge m">MISSED</span>':(agent?`<span class="badge">${agent}</span>`:'')}</div>
-          ${c.msg?`<div class="msg">“${c.msg}”</div>`:''}
-          ${(c.browsed&&c.browsed[0])?`<div class="brows">🛍️ ${c.browsed[0]}</div>`:''}
+          <div><span class="who">${flag(c.country_code)} ${esc(c.city||c.country)}</span>
+            <span class="loc"> · ${esc(c.country)} · ${esc(c.platform||'')} · ${c.type==='offline_msg'?'offline msg':'chat'}</span>
+            ${c.missed?'<span class="badge m">MISSED</span>':(agent?`<span class="badge">${esc(agent)}</span>`:'')}</div>
+          ${c.msg?`<div class="msg">“${esc(c.msg)}”</div>`:''}
+          ${(c.browsed&&c.browsed[0])?`<div class="brows">🛍️ ${esc(c.browsed[0])}</div>`:''}
         </div></div>`;
     }).join('')+'</div>';
     livePrevIds = new Set(ch.map(c=>c.id)); liveFirst = false;
diff --git a/server.js b/server.js
index e0dd418..e4dffa7 100644
--- a/server.js
+++ b/server.js
@@ -20,7 +20,7 @@ http.createServer((req, res) => {
   }
   if (req.method === 'POST' && req.url === '/api/refresh') {
     execFile('/usr/bin/python3', [path.join(__dirname, 'pull.py')],
-      { env: { ...process.env, START: '1770000000', MAX_PAGES: '40' } },
+      { env: { ...process.env, START: '1770000000', MAX_PAGES: '40' }, maxBuffer: 8 * 1024 * 1024 },
       (err, so, se) => {
         res.writeHead(err ? 500 : 200, { 'content-type': 'application/json' });
         res.end(JSON.stringify({ ok: !err, out: (so||'').trim(), err: (se||'').trim() }));
@@ -47,7 +47,7 @@ function refreshData(reason) {
 }
 try {
   const dj = path.join(ROOT, 'data.json');
-  const stale = !fs.existsSync(dj) || (Date.now() - fs.statSync(dj).mtimeMs) > REFRESH_HOURS * 3600e3;
+  const stale = !fs.existsSync(dj) || (Date.now() - fs.statSync(dj).mtimeMs) > REFRESH_HOURS * 3600 * 1000;
   if (stale) setTimeout(() => refreshData('boot'), 4000);
 } catch (e) {}
 setInterval(() => refreshData('interval'), REFRESH_HOURS * 3600 * 1000);

← 0cde498 Add '💬 Go Live in Zendesk' button — opens agent console for  ·  back to Zendesk Chat Analyzer  ·  href-drill:exempt marker — not a browse-grid (Steve's exclus c5ed5c0 →