← back to Rentv
auto-save: 2026-07-23T15:52:21 (4 files) — public/index.html server.js public/pulse.html tools/specs/pulse.json
f8414c0d4bdf8edba6f6a2b2ba4ac62c931f2744 · 2026-07-23 15:52:26 -0700 · Steve Abrams
Files touched
M public/index.htmlA public/pulse.htmlM server.jsA tools/specs/pulse.json
Diff
commit f8414c0d4bdf8edba6f6a2b2ba4ac62c931f2744
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 15:52:26 2026 -0700
auto-save: 2026-07-23T15:52:21 (4 files) — public/index.html server.js public/pulse.html tools/specs/pulse.json
---
public/index.html | 2 +-
public/pulse.html | 133 +++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 27 ++++++++++
tools/specs/pulse.json | 23 +++++++++
4 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/public/index.html b/public/index.html
index 1c385064..8394f393 100644
--- a/public/index.html
+++ b/public/index.html
@@ -104,7 +104,7 @@
<div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
<div class="snap" id="snap"></div>
<div class="grp"><div class="gt">Discover</div></div>
- <nav><a href="/search">🔎 Search</a></nav>
+ <nav><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a></nav>
<div class="grp"><div class="gt">Sections</div></div>
<nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
<div class="grp"><div class="gt">Intelligence</div></div>
diff --git a/public/pulse.html b/public/pulse.html
new file mode 100644
index 00000000..b328b151
--- /dev/null
+++ b/public/pulse.html
@@ -0,0 +1,133 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Market Pulse — RENTV.com</title>
+<meta name="description" content="RENTV Market Pulse — live commercial real estate transaction volume, deal count and sector breakdowns across the Western U.S.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8;--up:#0f8a4d}
+ *{box-sizing:border-box;margin:0}
+ body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+ .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+ .burger:hover{background:rgba(0,0,0,.06)}
+ .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+ .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+ .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+ .user:hover{border-color:var(--red);color:var(--red)}
+ .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+ .scrim.open{opacity:1;pointer-events:auto}
+ .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+ .drawer.open{transform:none}
+ .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+ .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+ .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+ .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+ .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+ .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+ .drawer nav a.active{background:#141a21;border-left-color:var(--red);color:#fff;font-weight:700}
+ .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+
+ .wrap{max-width:1120px;margin:0 auto;padding:0 22px;width:100%}
+ .phead{padding:34px 0 8px;display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:10px}
+ .phead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+ .phead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.6vw,46px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+ .phead .upd{color:var(--sub);font-size:12px;font-weight:600}
+ .kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;padding:14px 0 8px}
+ .kpi{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 16px 14px}
+ .kpi .v{font-family:'Playfair Display',serif;font-size:30px;font-weight:800;letter-spacing:-.01em}
+ .kpi .l{color:var(--sub);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-top:4px}
+ .kpi.hot{background:var(--red);color:#fff;border-color:var(--red)}.kpi.hot .l{color:rgba(255,255,255,.85)}
+ .cols{display:grid;grid-template-columns:1fr 1fr;gap:26px;padding:20px 0}
+ .card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px 20px}
+ .card h3{font-size:12px;text-transform:uppercase;letter-spacing:.09em;border-top:2px solid var(--ink);padding-top:10px;margin-bottom:14px;font-weight:800}
+ .bar{margin-bottom:13px}
+ .bar .r{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:4px}
+ .bar .r b{font-weight:700}.bar .r .n{color:var(--sub);font-weight:600}
+ .bar .track{height:9px;background:var(--wash);border-radius:6px;overflow:hidden}
+ .bar .fill{height:100%;background:var(--red);border-radius:6px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+ .bar.alt .fill{background:#0b0e12}
+ .full{grid-column:1/-1}
+ .tdeal{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;padding:11px 0;border-bottom:1px solid var(--line)}
+ .tdeal:last-child{border-bottom:0}
+ .tdeal h4{font:600 15px/1.3 'Inter'}
+ .tdeal .m{color:var(--sub);font-size:12px;margin-top:2px}
+ .tdeal .amt{font-family:'Playfair Display',serif;font-weight:800;font-size:19px;color:var(--red);white-space:nowrap}
+ .tdeal:hover h4{color:var(--red)}
+ .foot{padding:6px 0 50px;color:var(--sub);font-size:12px}
+ @media(max-width:860px){.kpis{grid-template-columns:repeat(2,1fr)}.cols{grid-template-columns:1fr}}
+</style></head><body>
+
+<header class="top">
+ <button class="burger" id="burger" aria-label="Menu">
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
+ <span class="lbl">Menu</span>
+ </button>
+ <a class="brand" href="/">REN<span>TV</span></a>
+ <a class="user" href="/search" aria-label="Search" title="Search">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+ </a>
+</header>
+
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+ <div class="dh"><div class="w">REN<span>TV</span></div><button id="dx" aria-label="Close">×</button></div>
+ <div class="grp"><div class="gt">Discover</div></div>
+ <nav><a href="/search">Search</a><a href="/pulse" class="active">Market Pulse</a></nav>
+ <div class="grp"><div class="gt">Sections</div></div>
+ <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+ <div class="grp"><div class="gt">Intelligence</div></div>
+ <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker & Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+ <div class="grp"><div class="gt">Company</div></div>
+ <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
+ <div class="df">RENTV.com — Commercial real estate news, Western U.S.</div>
+</aside>
+
+<main class="wrap">
+ <div class="phead">
+ <div><div class="k">Market Pulse</div><h1>Western U.S. deal flow, at a glance</h1></div>
+ <div class="upd" id="upd"></div>
+ </div>
+ <section class="kpis" id="kpis"></section>
+ <section class="cols">
+ <div class="card"><h3>Transaction volume by sector</h3><div id="byType"></div></div>
+ <div class="card"><h3>Deal count by transaction type</h3><div id="byTxn"></div></div>
+ <div class="card"><h3>Volume by state</h3><div id="byState"></div></div>
+ <div class="card" id="topcard"><h3>Largest tracked deals</h3><div id="top"></div></div>
+ </section>
+ <div class="foot" id="foot"></div>
+</main>
+
+<script>
+const $=id=>document.getElementById(id);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const drawer=$('drawer'),scrim=$('scrim');
+$('burger').onclick=()=>{drawer.classList.add('open');scrim.classList.add('open')};
+const close=()=>{drawer.classList.remove('open');scrim.classList.remove('open')};
+$('dx').onclick=close;scrim.onclick=close;document.addEventListener('keydown',e=>{if(e.key==='Escape')close()});
+
+function bars(el,rows,valKey,labelKey,alt){
+ const max=Math.max(...rows.map(r=>r[valKey]),1);
+ el.innerHTML=rows.map(r=>`<div class="bar${alt?' alt':''}"><div class="r"><b>${esc(r.name)}</b><span class="n">${esc(r[labelKey])} · ${r.count} deal${r.count===1?'':'s'}</span></div><div class="track"><div class="fill" style="width:${Math.max(4,Math.round(r[valKey]/max*100))}%"></div></div></div>`).join('');
+}
+
+(async function(){
+ let d; try{ d=await (await fetch('/api/pulse')).json(); }catch(e){ $('kpis').innerHTML='<div class="foot">Pulse data unavailable.</div>'; return; }
+ const k=d.kpis;
+ $('kpis').innerHTML=[
+ ['hot',k.totalVolumeLabel,'Tracked volume'],
+ ['',k.dealCount,'Deals in feed'],
+ ['',k.avgSizeLabel,'Avg deal size'],
+ ['',k.marketCount,'Markets'],
+ ['',k.tracked,'Priced deals'],
+ ].map(([c,v,l])=>`<div class="kpi ${c}"><div class="v">${esc(v)}</div><div class="l">${esc(l)}</div></div>`).join('');
+ bars($('byType'),d.byPropertyType,'vol','volLabel',false);
+ bars($('byTxn'),d.byTxnType.slice().sort((a,b)=>b.count-a.count).map(r=>({...r,vol:r.count,volLabel:r.count+'×'})),'vol','volLabel',true);
+ bars($('byState'),d.byState,'vol','volLabel',false);
+ $('top').innerHTML=d.topDeals.map(t=>`<a class="tdeal" href="${esc(t.href)}"><div><h4>${esc(t.title)}</h4><div class="m">${[t.type,t.txn,t.where].filter(Boolean).map(esc).join(' · ')}</div></div><div class="amt">${esc(t.label)}</div></a>`).join('');
+ if(d.updated){const dt=new Date(d.updated);$('upd').textContent='Updated '+dt.toLocaleDateString(undefined,{month:'short',day:'numeric'})+', '+dt.toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'});}
+ $('foot').textContent='Aggregated from RENTV’s tracked transaction feed. Figures reflect deals with a disclosed price.';
+})();
+</script>
+</body></html>
diff --git a/server.js b/server.js
index 51ab22ae..a1c72c88 100644
--- a/server.js
+++ b/server.js
@@ -459,5 +459,32 @@ app.get('/api/search', (req, res) => {
res.json({ q, total, groups });
});
+// ── CRE Market Pulse — analytics layer over the tracked deals feed. ──
+app.get('/api/pulse', (_req, res) => {
+ const file = readJSON('deals.json', {});
+ const deals = file.deals || [];
+ res.set('Cache-Control', 'public, max-age=300');
+ const withAmt = deals.filter((d) => typeof d.amount === 'number' && d.amount > 0);
+ const fmt = (n) => (n >= 1e9 ? '$' + (n / 1e9).toFixed(1) + 'B' : n >= 1e6 ? '$' + (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? '$' + Math.round(n / 1e3) + 'K' : '$' + Math.round(n));
+ const totalVol = withAmt.reduce((s, d) => s + d.amount, 0);
+ const avg = withAmt.length ? totalVol / withAmt.length : 0;
+ const group = (key) => {
+ const m = {};
+ deals.forEach((d) => { const k = d[key] || 'Other'; (m[k] = m[k] || { count: 0, vol: 0 }); m[k].count++; if (typeof d.amount === 'number') m[k].vol += d.amount; });
+ return Object.entries(m).map(([name, v]) => ({ name, count: v.count, vol: v.vol, volLabel: fmt(v.vol) })).sort((a, b) => b.vol - a.vol || b.count - a.count);
+ };
+ const top = withAmt.slice().sort((a, b) => b.amount - a.amount).slice(0, 6)
+ .map((d) => ({ title: d.title, label: d.amount_label || fmt(d.amount), type: d.property_type || '', txn: d.txn_type || '', where: [d.city, d.state].filter(Boolean).join(', '), href: '/deals' }));
+ const markets = [...new Set(deals.map((d) => [d.city, d.state].filter(Boolean).join(', ')).filter(Boolean))];
+ res.json({
+ updated: file.fetched_at || null,
+ kpis: { totalVolumeLabel: fmt(totalVol), dealCount: deals.length, tracked: withAmt.length, avgSizeLabel: fmt(avg), marketCount: markets.length },
+ byPropertyType: group('property_type'),
+ byTxnType: group('txn_type'),
+ byState: group('state'),
+ topDeals: top,
+ });
+});
+
app.use(express.static(PUB, { extensions: ['html'] }));
app.listen(PORT, () => console.log('rentv-v1 (full site, gated, live data) on ' + PORT));
diff --git a/tools/specs/pulse.json b/tools/specs/pulse.json
new file mode 100644
index 00000000..7119c0c2
--- /dev/null
+++ b/tools/specs/pulse.json
@@ -0,0 +1,23 @@
+{
+ "slug": "pulse",
+ "title": "Market Pulse",
+ "subtitle": "Western U.S. deal flow, at a glance",
+ "voice": "Samantha",
+ "scenes": [
+ {
+ "url": "/pulse",
+ "say": "Market Pulse turns RENTV's deal feed into an instant read of the market: over one point three billion dollars in tracked volume, across thirty deals and sixteen markets.",
+ "caption": "The whole deal feed, distilled into live KPIs"
+ },
+ {
+ "url": "/pulse",
+ "say": "See exactly where the capital is going. Multifamily leads at over seven hundred fifty million dollars, ahead of industrial, office and retail — with sales dominating transaction type.",
+ "caption": "Volume by sector & transaction type — as bar charts"
+ },
+ {
+ "url": "/pulse#topcard",
+ "say": "And the largest tracked deals are ranked for you, each one linking straight through to the full story on RENTV.",
+ "caption": "Largest deals, ranked — one click to the full story"
+ }
+ ]
+}
← 80231abd tools: reusable $0 local feature-video generator (Chrome sho
·
back to Rentv
·
pulse: CRE Market Pulse dashboard (/pulse + /api/pulse) — KP 24a3fedc →