← 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
M public/app.jsM public/index.htmlA sg-block-test.js
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||' '}</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||' '}</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 →