[object Object]

← back to Zendesk Chat Analyzer

Add missed-chats drill-down (clickable Missed KPI + filter) + SG geo-proxy test script

c3ce3336f562d705204f91489c624c206d287011 · 2026-08-11 08:05:14 -0700 · Steve

Files touched

Diff

commit c3ce3336f562d705204f91489c624c206d287011
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 11 08:05:14 2026 -0700

    Add missed-chats drill-down (clickable Missed KPI + filter) + SG geo-proxy test script
---
 public/app.js     | 26 ++++++++++++++-----------
 public/index.html |  4 ++++
 sg-block-test.js  | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 11 deletions(-)

diff --git a/public/app.js b/public/app.js
index 07304e7..fd59b2e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,5 +1,5 @@
 /* DW Chat History Analyzer — client. Loads aggregated data.json (no secrets) and renders. */
-let DATA = {chats: []}, FILTER = {country: null}, WCMODE = 'titles', charts = {}, sortKey = 'ts', sortDir = -1;
+let DATA = {chats: []}, FILTER = {country: null, missed: false}, WCMODE = 'titles', charts = {}, sortKey = 'ts', sortDir = -1;
 
 const STOP = new Set(("the a an and or of to in for on with is are be it this that you your we our i "+
 "at as by from he she they them his her hi hello hey thanks thank please great day designer wallcoverings "+
@@ -8,7 +8,7 @@ const STOP = new Set(("the a an and or of to in for on with is are be it this th
 "good morning afternoon looking help am pm us united states http https www com").split(/\s+/));
 
 const fmtDate = t => { if(!t) return ''; const d = typeof t==='number' ? new Date(t*1000) : new Date(t); return isNaN(d) ? '' : d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); };
-const rows = () => FILTER.country ? DATA.chats.filter(c=>c.country===FILTER.country) : DATA.chats;
+const rows = () => DATA.chats.filter(c => (!FILTER.country || c.country===FILTER.country) && (!FILTER.missed || c.missed));
 
 function load(){
   fetch('data.json?_='+Date.now()).then(r=>r.json()).then(d=>{ DATA=d; boot(); })
@@ -23,11 +23,15 @@ function renderAll(){ renderFilter(); kpis(); country(); type(); platform(); age
 
 function renderFilter(){
   const fb=document.getElementById('filterbar');
-  fb.innerHTML = FILTER.country
-    ? `Filtered to <b>${FILTER.country}</b> (${rows().length} chats) <span class="clr" onclick="clearF()">✕ clear</span>`
-    : `Showing all <b>${DATA.chats.length}</b> chats`;
+  const parts=[];
+  if(FILTER.country) parts.push(`country = <b>${FILTER.country}</b>`);
+  if(FILTER.missed) parts.push(`<b>missed / unanswered</b> chats only 📞`);
+  fb.innerHTML = parts.length
+    ? `Filtered to ${parts.join(' + ')} — <b>${rows().length}</b> chats <span class="clr" onclick="clearF()">✕ clear</span>`
+    : `Showing all <b>${DATA.chats.length}</b> chats · <span class="clr" onclick="showMissed()">📞 show ${DATA.chats.filter(c=>c.missed).length} missed chats</span>`;
 }
-window.clearF=()=>{ FILTER.country=null; renderAll(); };
+window.clearF=()=>{ FILTER.country=null; FILTER.missed=false; renderAll(); };
+window.showMissed=()=>{ FILTER.missed=true; sortKey='ts'; sortDir=-1; renderAll(); document.querySelector('#tbl').scrollIntoView({behavior:'smooth'}); };
 
 function kpis(){
   const r=rows();
@@ -38,11 +42,11 @@ function kpis(){
   const ratedBad=r.filter(c=>c.rating==='bad').length;
   const ts=r.map(c=>c.ts).filter(Boolean).sort();
   const span = ts.length? `${fmtDate(ts[0]).split(',')[0]} → ${fmtDate(ts[ts.length-1]).split(',')[0]}`:'—';
-  const K=[['Chats',r.length,span],['Countries',ctry,''],['Live chats',r.length-off,''],
-    ['Offline msgs',off,''],['Missed',missed,missed?'follow-up':''],
-    ['Rated 👍/👎',`${rated}/${ratedBad}`,'']];
-  document.getElementById('kpis').innerHTML=K.map(([l,n,s])=>
-    `<div class="kpi"><div class="n">${n}</div><div class="l">${l}</div><div class="s">${s||'&nbsp;'}</div></div>`).join('');
+  const K=[['Chats',r.length,span,null],['Countries',ctry,'',null],['Live chats',r.length-off,'',null],
+    ['Offline msgs',off,'',null],['Missed',missed,missed?'👆 click to follow up':'','showMissed()'],
+    ['Rated 👍/👎',`${rated}/${ratedBad}`,'',null]];
+  document.getElementById('kpis').innerHTML=K.map(([l,n,s,click])=>
+    `<div class="kpi${click?' clk':''}"${click?` onclick="${click}"`:''}><div class="n">${n}</div><div class="l">${l}</div><div class="s">${s||'&nbsp;'}</div></div>`).join('');
 }
 
 function mkChart(id,cfg){ if(charts[id])charts[id].destroy(); charts[id]=new Chart(document.getElementById(id),cfg); }
diff --git a/public/index.html b/public/index.html
index 2fe7a03..5746962 100644
--- a/public/index.html
+++ b/public/index.html
@@ -22,6 +22,10 @@ button:hover{border-color:var(--acc)}
 .kpi .n{font-size:26px;font-weight:800}
 .kpi .l{color:var(--dim);font-size:12px;text-transform:uppercase;letter-spacing:.04em}
 .kpi .s{font-size:11px;color:var(--dim);margin-top:2px}
+.kpi.clk{cursor:pointer;transition:border-color .15s}
+.kpi.clk:hover{border-color:var(--warn)}
+.kpi.clk:hover .s{color:var(--warn)}
+.clr{color:var(--acc);cursor:pointer}
 .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}
 .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;min-width:0}
 .card h3{margin:0 0 12px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;font-weight:700}
diff --git a/sg-block-test.js b/sg-block-test.js
new file mode 100644
index 0000000..a22a3ea
--- /dev/null
+++ b/sg-block-test.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+// Load the DW storefront through a Singapore-geolocated Browserbase browser and
+// determine whether Blockify actually blocks it. Reports final URL + block verdict.
+const { chromium } = require('/Users/macstudio3/Projects/all-designerwallcoverings/node_modules/playwright-core');
+const fs = require('fs');
+
+function env(k){ for(const l of fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n')) if(l.startsWith(k+'=')) return l.slice(k.length+1).trim().replace(/^["']|["']$/g,''); }
+const KEY = env('BROWSERBASE_API_KEY'), PROJ = env('BROWSERBASE_PROJECT_ID');
+const STORE = 'https://www.designerwallcoverings.com/';
+
+(async () => {
+  // 1. create SG-geolocated session
+  const r = await fetch('https://api.browserbase.com/v1/sessions', {
+    method: 'POST',
+    headers: { 'X-BB-API-Key': KEY, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ projectId: PROJ, proxies: [{ type: 'browserbase', geolocation: { country: 'SG', city: 'SINGAPORE' } }] }),
+  });
+  const sess = await r.json();
+  if (!sess.id) { console.log('SESSION FAIL:', JSON.stringify(sess).slice(0,300)); process.exit(1); }
+  console.log('BB session:', sess.id, '(SG proxy)');
+  const connectUrl = sess.connectUrl || `wss://connect.browserbase.com?apiKey=${KEY}&sessionId=${sess.id}`;
+
+  let verdict = 'UNKNOWN';
+  try {
+    const browser = await chromium.connectOverCDP(connectUrl);
+    const ctx = browser.contexts()[0] || await browser.newContext();
+    const page = ctx.pages()[0] || await ctx.newPage();
+    // confirm we're actually on a SG IP
+    let ipgeo = {};
+    try { await page.goto('https://ipapi.co/json/', { timeout: 30000 }); ipgeo = JSON.parse(await page.locator('body').innerText()); } catch(e){}
+    console.log('exit IP country:', ipgeo.country_name || '?', '| ip:', ipgeo.ip || '?');
+
+    await page.goto(STORE, { waitUntil: 'domcontentloaded', timeout: 45000 });
+    await page.waitForTimeout(6000); // let Blockify client script run + redirect
+    const finalUrl = page.url();
+    const title = await page.title().catch(()=> '');
+    const bodytext = (await page.locator('body').innerText().catch(()=> '')).slice(0, 2000).toLowerCase();
+    await page.screenshot({ path: '/Users/macstudio3/sg-block-test.png' }).catch(()=>{});
+
+    const blockedSignals = ['access denied','you have been blocked','not available in your','restricted in your',
+      'blockify','access to this store','region is restricted','cannot access','blocked'];
+    const hit = blockedSignals.find(s => bodytext.includes(s) || finalUrl.toLowerCase().includes('block'));
+    const storeLoaded = title.toLowerCase().includes('designer wallcoverings') && bodytext.includes('wallcovering') && !hit;
+
+    if (hit) verdict = 'BLOCKED ✅ (Singapore hit a block: "'+hit+'")';
+    else if (storeLoaded) verdict = 'NOT BLOCKED ❌ (Singapore loaded the normal store)';
+    else verdict = 'INCONCLUSIVE (title="'+title+'")';
+
+    console.log('final URL:', finalUrl);
+    console.log('title:', title);
+    console.log('body sample:', bodytext.slice(0,180).replace(/\n/g,' '));
+    console.log('VERDICT:', verdict);
+    await browser.close();
+  } catch (e) {
+    console.log('NAV ERROR:', String(e).slice(0,200));
+  }
+})();

← acdb19e Zendesk chat-history web viewer: KPIs, country/type/platform  ·  back to Zendesk Chat Analyzer  ·  Add themes classification + brand extraction + theme/brand c b875bb5 →