← back to Rentv 2026
PR intel: correspondence timeline on the contact drawer
ca159afbcb6df847cb7a73b011f3659689a836c8 · 2026-08-06 09:24:03 -0700 · Steve
Contact detail now shows a Correspondence section: every Gmail-synced message
to/from the contact, direction (▲out/▼in), subject, and real date+time
(toLocaleString — date AND time per the admin-card rule). Empty state prompts
to connect Gmail on the Credentials page. Reads GET /people/:id/timeline
(tenant-scoped). Verified against the live endpoint.
Files touched
M public/admin/pr-intelligence/people.html
Diff
commit ca159afbcb6df847cb7a73b011f3659689a836c8
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 09:24:03 2026 -0700
PR intel: correspondence timeline on the contact drawer
Contact detail now shows a Correspondence section: every Gmail-synced message
to/from the contact, direction (▲out/▼in), subject, and real date+time
(toLocaleString — date AND time per the admin-card rule). Empty state prompts
to connect Gmail on the Credentials page. Reads GET /people/:id/timeline
(tenant-scoped). Verified against the live endpoint.
---
public/admin/pr-intelligence/people.html | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/public/admin/pr-intelligence/people.html b/public/admin/pr-intelligence/people.html
index 5828ddf6..790cfd18 100644
--- a/public/admin/pr-intelligence/people.html
+++ b/public/admin/pr-intelligence/people.html
@@ -113,6 +113,8 @@
<h3 style="margin:14px 0 8px">Outreach</h3>
${p.outreach.map((m) => `<div class="gatechk">${PR.statusBadge(m.status)} <span>${PR.esc(m.subject || '(no subject)')}</span>
<span style="color:var(--muted);margin-left:auto">${PR.dateShort(m.sent_at || m.drafted_at)}</span></div>`).join('') || '<div class="empty">No outreach yet</div>'}
+ <h3 style="margin:14px 0 8px">Correspondence <span style="font-weight:400;color:var(--muted);font-size:12px">(email dates & times, synced from Gmail)</span></h3>
+ <div id="e-timeline"><div class="empty">Loading…</div></div>
<h3 style="margin:14px 0 8px">Audit trail</h3>
${p.recent_activity.slice(0, 12).map((a) => `<div class="gatechk"><span style="color:var(--muted)">${PR.dateShort(a.created_at)}</span>
<span>${PR.esc(a.activity)}</span><span style="color:var(--muted)">${PR.esc(a.actor || '')}</span></div>`).join('')}`);
@@ -122,6 +124,16 @@
? '<div class="b green" style="margin-top:8px">✅ Outreach-ready</div>'
: '<div class="evi" style="border-color:var(--warn)"><b>Not outreach-ready:</b> ' + r.reasons.map(PR.esc).join('; ') + '</div>';
});
+ // Correspondence timeline (Gmail-synced messages, with real date + time)
+ PR.api('/people/' + id + '/timeline').then((r) => {
+ const el = body.querySelector('#e-timeline'); if (!el) return;
+ const rows = (r && r.rows) || [];
+ if (!rows.length) { el.innerHTML = '<div class="empty">No correspondence yet — connect Gmail on the Credentials page and run a sync.</div>'; return; }
+ const arrow = (d) => d === 'inbound' ? '<span title="received" style="color:#0a8a4a">▼ in</span>' : '<span title="sent" style="color:#0b6cff">▲ out</span>';
+ const when = (m) => { const t = m.corresponded_at || m.sent_at || m.replied_at; return t ? new Date(t).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '—'; };
+ el.innerHTML = rows.map((m) => `<div class="gatechk">${arrow(m.direction)} <span>${PR.esc(m.subject || '(no subject)')}</span>
+ <span style="color:var(--muted);margin-left:auto" title="${PR.esc(m.corresponded_at || '')}">🕓 ${PR.esc(when(m))}</span></div>`).join('');
+ }).catch(() => { const el = body.querySelector('#e-timeline'); if (el) el.innerHTML = '<div class="empty">Timeline unavailable.</div>'; });
body.querySelector('#e-save').addEventListener('click', async () => {
await PR.guard(() => PR.api('/people/' + id, { method: 'PUT', body: {
exact_title: body.querySelector('#e-title').value,
← df653c37 auto-save: 2026-08-06T09:20:28 (10 files) — .deploy.conf dat
·
back to Rentv 2026
·
News map: market-activity stats panel (count + deal volume + 27f2cd04 →