Wallet-verified member messaging (matrix-lines permissions, admin visibility)

- messages.js: challenge/personal_sign/recover auth (vendored pinned
  js-sha3 0.9.3 + noble-secp256k1 1.7.1, server-side only; self-tested
  positive + tamper cases), 30d HttpOnly sessions, message store on the
  volume, matrix-line permissions (your downline direct or broadcast, your
  upline chain - nothing else, so spam is impossible by construction),
  daily rate limits (30 direct / 3 broadcasts), 1500-char plain text
- chain.js: memberIdByAccount (wallet -> position for sign-in)
- API: msg-challenge/-verify/-me/-inbox/-send/-read public + msg-unread
  (count only, no auth) + admin/messages (full visibility, disclosed to
  members in the UI)
- Dashboard: Messages card with unread bell, one-tap wallet sign-in,
  inbox with auto-read, compose with to-ID or whole-team broadcast
- Admin: Member Messages review table
- Chatbot canned answer + AI system prompt updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-19 13:31:31 -05:00
parent e31b01e338
commit 9a1833f2b0
10 changed files with 2188 additions and 4 deletions
+10
View File
@@ -41,6 +41,7 @@ function render(){
const orgEl=document.getElementById('orgRoot');if(orgEl&&!orgEl.value)orgEl.value=state.config.orgRootId||'21';
if(!orgShareAutoLoaded&&orgEl&&orgEl.value){orgShareAutoLoaded=true;loadOrgShare();}
if(!coachAutoLoaded){coachAutoLoaded=true;loadCoaching();}
if(!msgsAutoLoaded){msgsAutoLoaded=true;loadAdminMsgs();}
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;
}
@@ -121,6 +122,15 @@ async function loadCoaching(){
}
const cr=document.getElementById('coachRefresh');if(cr)cr.addEventListener('click',loadCoaching);
let coachAutoLoaded=false;
async function loadAdminMsgs(){
const tb=document.getElementById('msgRows');if(!tb)return;
try{
const d=await api('/api/admin/messages');
tb.innerHTML=(d.messages||[]).map(m=>`<tr><td>${esc(new Date(m.ts).toLocaleString())}</td><td><strong>#${m.fromId}</strong></td><td>${m.org?'📣 whole team':'#'+m.toId}</td><td style="white-space:pre-wrap;max-width:460px">${esc(m.body)}</td><td>${m.readCount}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No messages yet.</td></tr>';
}catch(x){tb.innerHTML=`<tr><td colspan="5" class="empty">${esc(x.message)}</td></tr>`}
}
const mr=document.getElementById('msgRefresh');if(mr)mr.addEventListener('click',loadAdminMsgs);
let msgsAutoLoaded=false;
let incomeAutoLoaded=false;
async function loadIncome(){
const ids=(document.getElementById('incomeIds').value||'').trim();