Dashboard: let members see what a nudge actually says before and after sending

A member sent a nudge to #102, then went looking for what had gone out. The
only feedback was a small "Delivered to their dashboard" note, so she checked
her clipboard - which still held the unrelated "Copy plain-English summary"
text describing her OWN position. She reasonably concluded the nudge had sent
her position instead of the recipient's.

It had not: the message log shows #34 -> #102 carrying #102's own numbers.
The feature was right and the feedback was wrong, which is its own kind of
bug - a member who cannot verify what they sent will assume the worst.

Every nudge now has a Preview button that names the recipient and shows the
exact text before sending, and on success the panel shows what was sent and
to whom rather than a bare "Delivered".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-31 09:42:59 -05:00
parent 64527af2ab
commit 34480d444e
3 changed files with 72 additions and 5 deletions
+18
View File
@@ -0,0 +1,18 @@
import pkg from 'file:///D:/Projects/MarketingAgent/qa-tester/node_modules/playwright/index.js';
const { chromium } = pkg;
const b = await chromium.connectOverCDP('http://127.0.0.1:9222');
const page = await b.contexts()[0].newPage();
await page.setViewportSize({ width: 1500, height: 1000 });
await page.goto('https://www.adrevsplit.com/aradmin/admin.php?b=4411', { waitUntil:'domcontentloaded', timeout:45000 });
await page.waitForTimeout(2500);
const r = await page.evaluate(() => {
const rows = [...document.querySelectorAll('table tr')].map(tr =>
[...tr.querySelectorAll('td,th')].map(td => td.innerText.replace(/\s+/g,' ').trim()));
const links = [...document.querySelectorAll('a[href*="rmcircle"], a[href*="join"]')].map(a=>a.getAttribute('href'));
return { rows: rows.filter(r2=>r2.length>3).slice(0,14), links: [...new Set(links)] };
});
console.log('=== approved login ads ===');
r.rows.forEach(x => console.log(' ' + x.join(' | ').slice(0,150)));
console.log('\n=== destination links found ===');
r.links.forEach(l => console.log(' ' + l));
await page.close(); await b.close();
+28
View File
@@ -0,0 +1,28 @@
import pkg from 'file:///D:/Projects/MarketingAgent/qa-tester/node_modules/playwright/index.js';
const { chromium } = pkg;
const b = await chromium.connectOverCDP('http://127.0.0.1:9222');
const page = await b.contexts()[0].newPage();
await page.setViewportSize({ width: 1340, height: 1000 });
await page.goto('https://adrevsplit.com/login.php', { waitUntil:'domcontentloaded', timeout:45000 });
await page.waitForTimeout(3000);
console.log(JSON.stringify(await page.evaluate(() => {
const col = document.querySelector('.col-lg-10, .custom-col-10');
const out = { colTop: col ? Math.round(col.getBoundingClientRect().top) : null, children: [] };
if (col) for (const c of col.children) {
if (c.getBoundingClientRect().height === 0) continue;
const r = c.getBoundingClientRect(); const cs = getComputedStyle(c);
out.children.push({
tag: c.tagName, cls: (c.className||'').toString().slice(0,30),
top: Math.round(r.top), h: Math.round(r.height),
mt: cs.marginTop, mb: cs.marginBottom,
text: (c.innerText||'').replace(/\s+/g,' ').trim().slice(0,40)
});
}
const ul = document.querySelector('.flexy-menu');
out.menuTop = ul ? Math.round(ul.getBoundingClientRect().top) : null;
const sec = document.querySelector('section.container');
out.sectionTop = sec ? Math.round(sec.getBoundingClientRect().top) : null;
out.sectionPadTop = sec ? getComputedStyle(sec).paddingTop : null;
return out;
}, null), null, 1));
await b.close();
+26 -5
View File
@@ -399,7 +399,18 @@
// One-tap nudges: pre-written message per coach recommendation, sent over // One-tap nudges: pre-written message per coach recommendation, sent over
// the wallet-verified Messages rails (and bridged to Telegram if linked). // the wallet-verified Messages rails (and bridged to Telegram if linked).
const nudgeTexts={}; const nudgeTexts={};
function nudgeBtn(key){return `<br><button class="btn btn-teal btn-sm coach-nudge" data-nkey="${key}" style="margin-top:7px">📨 Send this nudge</button><span class="micro coach-nudge-note" style="display:none;margin-left:8px"></span>`} // Nudges used to send silently with only a "Delivered" note, so nobody could
// see WHAT went out or to whom. A member went hunting in her clipboard, found
// the unrelated "copy my summary" text still sitting there, and reasonably
// concluded the nudge had sent her own position instead of the recipient's.
// Preview before, and the exact sent text after, removes the guesswork.
function nudgeBtn(key){return `<div class="coach-nudge-wrap" style="margin-top:7px">`+
`<button class="btn btn-teal btn-sm coach-nudge" data-nkey="${key}">📨 Send this nudge</button>`+
`<button class="btn btn-secondary btn-sm coach-nudge-preview" data-nkey="${key}" style="margin-left:6px">👁 Preview</button>`+
`<span class="micro coach-nudge-note" style="display:none;margin-left:8px"></span>`+
`<div class="coach-nudge-prev" style="display:none;margin-top:8px;background:rgba(78,214,203,.06);`+
`border:1px solid rgba(78,214,203,.28);border-radius:10px;padding:10px 12px;font-size:12.5px;`+
`line-height:1.6;color:var(--muted);white-space:pre-wrap"></div></div>`}
function regNudge(id,kind,text){const key=id+':'+kind;nudgeTexts[key]={to:id,text};return nudgeBtn(key)} function regNudge(id,kind,text){const key=id+':'+kind;nudgeTexts[key]={to:id,text};return nudgeBtn(key)}
function renderCoach(d){ function renderCoach(d){
const card=document.getElementById('dCoachCard'),el=document.getElementById('dCoach'); const card=document.getElementById('dCoachCard'),el=document.getElementById('dCoach');
@@ -418,10 +429,20 @@
if(!el.__nudgeBound){ if(!el.__nudgeBound){
el.__nudgeBound=true; el.__nudgeBound=true;
el.addEventListener('click',async ev=>{ el.addEventListener('click',async ev=>{
const pv=ev.target.closest&&ev.target.closest('button.coach-nudge-preview');
const btn=ev.target.closest&&ev.target.closest('button.coach-nudge'); const btn=ev.target.closest&&ev.target.closest('button.coach-nudge');
if(!btn)return; if(!pv&&!btn)return;
const rec=nudgeTexts[btn.dataset.nkey];if(!rec)return; const rec=nudgeTexts[(pv||btn).dataset.nkey];if(!rec)return;
const note=btn.nextElementSibling; const wrap=(pv||btn).closest('.coach-nudge-wrap');
const note=wrap&&wrap.querySelector('.coach-nudge-note');
const prev=wrap&&wrap.querySelector('.coach-nudge-prev');
const showPrev=(label)=>{if(!prev)return;prev.textContent=label+'\n\n'+rec.text;prev.style.display='';};
// Preview: show exactly what will be sent, and to whom, before sending.
if(pv){
if(prev&&prev.style.display!=='none'){prev.style.display='none';pv.textContent='\u{1F441} Preview';}
else{showPrev('This will be sent to #'+rec.to+':');pv.textContent='\u{1F441} Hide preview';}
return;
}
const say=(t,color)=>{if(note){note.style.display='';note.style.color=color||'var(--muted)';note.textContent=t;}}; const say=(t,color)=>{if(note){note.style.display='';note.style.color=color||'var(--muted)';note.textContent=t;}};
btn.disabled=true;const old=btn.textContent;btn.textContent='Sending…'; btn.disabled=true;const old=btn.textContent;btn.textContent='Sending…';
try{ try{
@@ -434,7 +455,7 @@
} }
const r=await fetch('/api/public/msg-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({toId:rec.to,body:rec.text})}); const r=await fetch('/api/public/msg-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({toId:rec.to,body:rec.text})});
const dd=await r.json().catch(()=>({})); const dd=await r.json().catch(()=>({}));
if(r.ok&&!dd.error){btn.textContent='Sent ✓';say('Delivered to their dashboard — and straight to their Telegram if they’ve linked it.');} if(r.ok&&!dd.error){btn.textContent='Sent ✓';showPrev('✅ Sent to #'+rec.to+' — this is exactly what they received:');const pb=wrap&&wrap.querySelector('.coach-nudge-preview');if(pb)pb.style.display='none';say('Delivered to #'+rec.to+'’s dashboard — and straight to their Telegram if they’ve linked it.');}
else{btn.textContent=old;btn.disabled=false;say(dd.error||'Could not send — try again.','var(--danger)');} else{btn.textContent=old;btn.disabled=false;say(dd.error||'Could not send — try again.','var(--danger)');}
}catch(e){btn.textContent=old;btn.disabled=false;say('Network hiccup — try again.','var(--danger)');} }catch(e){btn.textContent=old;btn.disabled=false;say('Network hiccup — try again.','var(--danger)');}
}); });