← back to Ventura Claw Leads
yolo tick 13: /map pin hover preview + /admin/billing recent activity feed
65ee0ecf24729fb170219aca641fd2daad656021 · 2026-05-06 19:51:52 -0700 · Steve Abrams
/MAP HOVER PREVIEW
- views/public/map.ejs: every Leaflet marker now has bindTooltip with the
business name + vertical kicker + neighborhood. Renders on hover (no
click needed). Tooltip themed to match the site (var(--bg)/--fg/--border)
with a subtle drop shadow.
- Tooltip arrow stripped via .leaflet-tooltip.pin-hover::before {display:none}
for a cleaner look against light + dark themes.
- popup (click) still works with the full action buttons.
/ADMIN/BILLING ACTIVITY FEED
- routes/admin.js: GET /admin/billing now also queries subscription_events
filtered by payload->>'metadata'->>'business_id' = current business id,
newest 20. JSON-path filter is the right approach since the same Stripe
account fires events to all sister projects (NPH, lawyer, etc.).
- views/admin/billing.ejs: 'Recent activity' table above 'Manage
subscription'. Per-row: timestamp, event_type (mono), detail (amount in
USD if amount_paid/amount_due present, else status). Status='active'
renders green.
46/46 tests still pass. /map and /admin/billing both 200/302 on prod.
Files touched
M routes/admin.jsM views/admin/billing.ejsM views/public/map.ejs
Diff
commit 65ee0ecf24729fb170219aca641fd2daad656021
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 19:51:52 2026 -0700
yolo tick 13: /map pin hover preview + /admin/billing recent activity feed
/MAP HOVER PREVIEW
- views/public/map.ejs: every Leaflet marker now has bindTooltip with the
business name + vertical kicker + neighborhood. Renders on hover (no
click needed). Tooltip themed to match the site (var(--bg)/--fg/--border)
with a subtle drop shadow.
- Tooltip arrow stripped via .leaflet-tooltip.pin-hover::before {display:none}
for a cleaner look against light + dark themes.
- popup (click) still works with the full action buttons.
/ADMIN/BILLING ACTIVITY FEED
- routes/admin.js: GET /admin/billing now also queries subscription_events
filtered by payload->>'metadata'->>'business_id' = current business id,
newest 20. JSON-path filter is the right approach since the same Stripe
account fires events to all sister projects (NPH, lawyer, etc.).
- views/admin/billing.ejs: 'Recent activity' table above 'Manage
subscription'. Per-row: timestamp, event_type (mono), detail (amount in
USD if amount_paid/amount_due present, else status). Status='active'
renders green.
46/46 tests still pass. /map and /admin/billing both 200/302 on prod.
---
routes/admin.js | 28 +++++++++++++++++++++-------
views/admin/billing.ejs | 29 +++++++++++++++++++++++++++++
views/public/map.ejs | 17 +++++++++++++++++
3 files changed, 67 insertions(+), 7 deletions(-)
diff --git a/routes/admin.js b/routes/admin.js
index d5bf018..98f06c9 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -306,13 +306,27 @@ router.post('/team/:id(\\d+)/remove', async (req, res, next) => {
function escapeHtml(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
// ─── Billing ───────────────────────────────────────────────────────────
-router.get('/billing', (req, res) => {
- res.render('admin/billing', {
- title: 'Billing · Ventura Claw',
- stripeLive: stripe.isLive(),
- tiers: stripe.VCL_TIERS,
- flash: req.query
- });
+router.get('/billing', async (req, res, next) => {
+ try {
+ // Recent Stripe events for THIS business — pulled from the audited
+ // subscription_events table, filtered by metadata.business_id matching
+ // the current business so a multi-tenant Stripe account stays clean.
+ const events = await db.many(`
+ SELECT id, stripe_event_id, event_type, payload, created_at
+ FROM subscription_events
+ WHERE (payload -> 'data' -> 'object' -> 'metadata' ->> 'business_id') = $1
+ ORDER BY created_at DESC
+ LIMIT 20
+ `, [String(res.locals.currentBusiness.id)]);
+
+ res.render('admin/billing', {
+ title: 'Billing · Ventura Claw',
+ stripeLive: stripe.isLive(),
+ tiers: stripe.VCL_TIERS,
+ events,
+ flash: req.query
+ });
+ } catch (err) { next(err); }
});
router.post('/billing/checkout', async (req, res, next) => {
diff --git a/views/admin/billing.ejs b/views/admin/billing.ejs
index 2cf8716..0285cf3 100644
--- a/views/admin/billing.ejs
+++ b/views/admin/billing.ejs
@@ -68,6 +68,35 @@
<% }); %>
</div>
+ <% if (typeof events !== 'undefined' && events && events.length > 0) { %>
+ <h2 style="margin-top:48px">Recent activity</h2>
+ <p class="muted" style="margin:0 0 16px;font-size:13px">Stripe events for your subscription, newest first.</p>
+ <table style="width:100%;border-collapse:collapse;font-size:13px">
+ <thead>
+ <tr style="border-bottom:1px solid var(--border);text-align:left">
+ <th style="padding:8px;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--fg-muted);font-weight:500">When</th>
+ <th style="padding:8px;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--fg-muted);font-weight:500">Event</th>
+ <th style="padding:8px;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--fg-muted);font-weight:500">Detail</th>
+ </tr>
+ </thead>
+ <tbody>
+ <% events.forEach(function(ev){
+ var obj = (ev.payload && ev.payload.data && ev.payload.data.object) || {};
+ var detail = '';
+ if (obj.amount_paid != null) detail = '$' + (obj.amount_paid / 100).toFixed(2);
+ else if (obj.amount_due != null) detail = '$' + (obj.amount_due / 100).toFixed(2);
+ else if (obj.status) detail = obj.status;
+ %>
+ <tr style="border-bottom:1px solid var(--border)">
+ <td style="padding:10px 8px;color:var(--fg-muted);white-space:nowrap"><%= new Date(ev.created_at).toLocaleString('en-US',{ month:'short', day:'numeric', hour:'numeric', minute:'2-digit' }) %></td>
+ <td style="padding:10px 8px;font-family:var(--mono,monospace);font-size:12px"><%= ev.event_type %></td>
+ <td style="padding:10px 8px;color:<%= obj.status === 'active' ? '#16a34a' : 'var(--fg-muted)' %>"><%= detail || '—' %></td>
+ </tr>
+ <% }); %>
+ </tbody>
+ </table>
+ <% } %>
+
<% if (currentBusiness.stripe_customer_id) { %>
<h2 style="margin-top:48px">Manage subscription</h2>
<p>Update your payment method, see invoices, or cancel from the secure Stripe portal.</p>
diff --git a/views/public/map.ejs b/views/public/map.ejs
index 775bfd0..39b3f6c 100644
--- a/views/public/map.ejs
+++ b/views/public/map.ejs
@@ -31,6 +31,14 @@
.pin-popup .pin-actions a.primary { background: #0e0e0e; color: #fff; }
.pin-popup .pin-actions a.ghost { background: transparent; color: #0e0e0e; border: 1px solid #ccc; }
.pin-popup .pin-headline { font-size: 0.78rem; color: #444; margin: 0 0 4px; line-height: 1.4; }
+ .leaflet-tooltip.pin-hover {
+ background: var(--bg, #fff); color: var(--fg, #0e0e0e);
+ border: 1px solid var(--border, #d8d2c0); border-radius: 4px;
+ padding: 6px 10px; font-family: var(--sans, -apple-system, sans-serif);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ font-size: 13px; line-height: 1.35;
+ }
+ .leaflet-tooltip.pin-hover::before { display: none; }
</style>
<section class="map-page">
@@ -127,6 +135,15 @@
if (typeof b.latitude !== 'number' || typeof b.longitude !== 'number') return;
var m = L.marker([b.latitude, b.longitude], { icon: pinIcon(b) });
m.bindPopup(popupHtml(b));
+ // Hover preview — quick name + vertical without forcing a click.
+ // 'sticky' keeps it pinned to the cursor on the marker; auto-hide on leave.
+ var verticalLabel = (b.vertical || '').replace(/_/g, ' ');
+ m.bindTooltip(
+ '<strong>' + escHtml(b.business_name) + '</strong>'
+ + '<br><span style="font-size:11px;color:#b8860b;letter-spacing:0.06em;text-transform:uppercase">' + escHtml(verticalLabel) + '</span>'
+ + (b.neighborhood ? '<br><span style="font-size:11px;color:#666">' + escHtml(b.neighborhood) + '</span>' : ''),
+ { direction: 'top', offset: [0, -8], opacity: 0.95, sticky: false, className: 'pin-hover' }
+ );
cluster.addLayer(m);
allMarkers.push({ data: b, marker: m });
});
← abf7235 yolo tick 12: 30-day lead-trend sparkline on admin dashboard
·
back to Ventura Claw Leads
·
yolo tick 14: /admin/leads search-within-messages 294e68a →