[object Object]

← back to Rentv

rentv: wire all 10 admin dashboards to live data (audience/news/videos/pulse/ticker) + finish front-page pulse stats + select-to-live gallery UI with LIVE badge (TK-10356)

daa67ba133bec0c96530a8f25e329df303bec73b · 2026-08-07 14:20:04 -0700 · Steve

Files touched

Diff

commit daa67ba133bec0c96530a8f25e329df303bec73b
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 14:20:04 2026 -0700

    rentv: wire all 10 admin dashboards to live data (audience/news/videos/pulse/ticker) + finish front-page pulse stats + select-to-live gallery UI with LIVE badge (TK-10356)
---
 public/admin/index.html    |  71 +++++++++++++++++-
 public/admin/render.js     |  25 +++++--
 public/versions/index.html | 175 +++++++++++++++++++++++++++++++++++++++------
 public/versions/render.js  |  43 +++++++++--
 4 files changed, 278 insertions(+), 36 deletions(-)

diff --git a/public/admin/index.html b/public/admin/index.html
index 49eafc57..ed847409 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -17,9 +17,74 @@ a{color:inherit;text-decoration:none}.wrap{max-width:1240px;margin:0 auto;paddin
 @media(max-width:820px){.aham{display:block}.anl{display:none;position:absolute;top:44px;left:0;right:0;background:var(--pan);flex-direction:column;align-items:stretch;border-bottom:1px solid var(--ln);box-shadow:0 12px 26px rgba(0,0,0,.4);z-index:30}.anl.open{display:flex}.anl a{padding:13px 20px;border-bottom:1px solid var(--ln)}}
 .hd{padding:44px 0 6px;text-align:center}.hd h1{font-family:'Playfair Display',serif;font-size:clamp(28px,5vw,48px)}.hd h1 span{color:var(--a)}.hd p{color:var(--sub);margin-top:8px}
 .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;padding:28px 0 60px}
-.vc{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:20px;transition:.2s}
+.vc{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:20px;transition:.2s;display:block;position:relative}
 .vc:hover{transform:translateY(-4px);border-color:var(--a);box-shadow:0 16px 40px rgba(0,0,0,.5)}
-.tag{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--a);font-weight:800}.vc h3{font-size:20px;margin:6px 0}.vc p{color:var(--sub);font-size:14px;min-height:40px}.vc .go{color:var(--a);font-weight:800;font-size:12px;text-transform:uppercase;letter-spacing:.06em}</style></head>
+.vc.is-live{border-color:var(--a);box-shadow:0 0 0 1px var(--a)}
+.tag{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--a);font-weight:800}.vc h3{font-size:20px;margin:6px 0}.vc p{color:var(--sub);font-size:14px;min-height:40px}
+.card-foot{display:flex;align-items:center;gap:10px;margin-top:14px}
+.vc .go{color:var(--a);font-weight:800;font-size:12px;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0}
+.live-badge{display:none;align-items:center;gap:5px;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#2ea043;background:rgba(46,160,67,.12);border:1px solid rgba(46,160,67,.3);border-radius:4px;padding:3px 8px;flex-shrink:0}
+.live-badge::before{content:'●';font-size:9px}
+.vc.is-live .live-badge{display:flex}
+.make-live-btn{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;background:transparent;border:1px solid var(--ln);color:var(--sub);border-radius:5px;padding:5px 10px;cursor:pointer;transition:.15s;flex-shrink:0}
+.make-live-btn:hover{border-color:var(--a);color:var(--ink);background:rgba(200,16,46,.08)}
+.vc.is-live .make-live-btn{border-color:var(--a);color:var(--a);background:rgba(200,16,46,.08);cursor:default}
+.make-live-btn:disabled{opacity:.5;cursor:not-allowed}
+</style></head>
 <body><div class="abar"><div class="wrap"><div class="abrand">REN<span>TV</span> Admin</div><a href="/">← main site</a></div></div>
 <header class="hd"><div class="wrap"><h1>REN<span>TV</span> · 10 Admin Versions</h1><p>Ten complete back-office directions over the real subscriber / feed / video / market data. Open each and pick the one to run.</p></div></header>
-<div class="wrap"><div class="grid"><a class="vc" href="/admin/classic"><div class="tag">Admin</div><h3>Classic Dashboard</h3><p>KPI cards up top, two data tables below. The safe, familiar admin.</p><span class="go">Open →</span></a><a class="vc" href="/admin/sidebar"><div class="tag">Admin</div><h3>Sidebar Console</h3><p>Left nav rail + main panel — the SaaS-standard admin shell.</p><span class="go">Open →</span></a><a class="vc" href="/admin/console"><div class="tag">Admin</div><h3>Ops Console</h3><p>Monospace metrics row + a live activity log. For the data-desk operator.</p><span class="go">Open →</span></a><a class="vc" href="/admin/cards"><div class="tag">Admin</div><h3>Module Grid</h3><p>Independent widget cards you can scan at a glance.</p><span class="go">Open →</span></a><a class="vc" href="/admin/table"><div class="tag">Admin</div><h3>Data Table</h3><p>Dense spreadsheet-style rows — power-user, keyboard-first feel.</p><span class="go">Open →</span></a><a class="vc" href="/admin/analytics"><div class="tag">Admin</div><h3>Analytics First</h3><p>One giant headline metric, then supporting stats and a table.</p><span class="go">Open →</span></a><a class="vc" href="/admin/split"><div class="tag">Admin</div><h3>Master–Detail</h3><p>Scrollable list on the left, KPI rail on the right.</p><span class="go">Open →</span></a><a class="vc" href="/admin/kanban"><div class="tag">Admin</div><h3>Kanban Board</h3><p>Stories grouped into columns by sector — editorial pipeline view.</p><span class="go">Open →</span></a><a class="vc" href="/admin/minimal"><div class="tag">Admin</div><h3>Minimal</h3><p>Quiet single-column stat sheet. No chrome, just the numbers.</p><span class="go">Open →</span></a><a class="vc" href="/admin/command"><div class="tag">Admin</div><h3>Command Center</h3><p>Multi-panel grid — every surface (subs, feed, video, market) at once.</p><span class="go">Open →</span></a></div></div></body></html>
\ No newline at end of file
+<div class="wrap"><div class="grid" id="gallery">
+  <div class="vc" data-key="classic"><div class="tag">Admin</div><h3>Classic Dashboard</h3><p>KPI cards up top, two data tables below. The safe, familiar admin.</p><div class="card-foot"><a class="go" href="/admin/classic">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('classic',this)">Make live</button></div></div>
+  <div class="vc" data-key="sidebar"><div class="tag">Admin</div><h3>Sidebar Console</h3><p>Left nav rail + main panel — the SaaS-standard admin shell.</p><div class="card-foot"><a class="go" href="/admin/sidebar">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('sidebar',this)">Make live</button></div></div>
+  <div class="vc" data-key="console"><div class="tag">Admin</div><h3>Ops Console</h3><p>Monospace metrics row + a live activity log. For the data-desk operator.</p><div class="card-foot"><a class="go" href="/admin/console">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('console',this)">Make live</button></div></div>
+  <div class="vc" data-key="cards"><div class="tag">Admin</div><h3>Module Grid</h3><p>Independent widget cards you can scan at a glance.</p><div class="card-foot"><a class="go" href="/admin/cards">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('cards',this)">Make live</button></div></div>
+  <div class="vc" data-key="table"><div class="tag">Admin</div><h3>Data Table</h3><p>Dense spreadsheet-style rows — power-user, keyboard-first feel.</p><div class="card-foot"><a class="go" href="/admin/table">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('table',this)">Make live</button></div></div>
+  <div class="vc" data-key="analytics"><div class="tag">Admin</div><h3>Analytics First</h3><p>One giant headline metric, then supporting stats and a table.</p><div class="card-foot"><a class="go" href="/admin/analytics">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('analytics',this)">Make live</button></div></div>
+  <div class="vc" data-key="split"><div class="tag">Admin</div><h3>Master–Detail</h3><p>Scrollable list on the left, KPI rail on the right.</p><div class="card-foot"><a class="go" href="/admin/split">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('split',this)">Make live</button></div></div>
+  <div class="vc" data-key="kanban"><div class="tag">Admin</div><h3>Kanban Board</h3><p>Stories grouped into columns by sector — editorial pipeline view.</p><div class="card-foot"><a class="go" href="/admin/kanban">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('kanban',this)">Make live</button></div></div>
+  <div class="vc" data-key="minimal"><div class="tag">Admin</div><h3>Minimal</h3><p>Quiet single-column stat sheet. No chrome, just the numbers.</p><div class="card-foot"><a class="go" href="/admin/minimal">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('minimal',this)">Make live</button></div></div>
+  <div class="vc" data-key="command"><div class="tag">Admin</div><h3>Command Center</h3><p>Multi-panel grid — every surface (subs, feed, video, market) at once.</p><div class="card-foot"><a class="go" href="/admin/command">Open →</a><span class="live-badge">LIVE</span><button class="make-live-btn" onclick="makeLive('command',this)">Make live</button></div></div>
+</div></div>
+<script>
+function setLiveBadge(key) {
+  document.querySelectorAll('#gallery .vc').forEach(function(card) {
+    var isLive = card.dataset.key === key;
+    card.classList.toggle('is-live', isLive);
+    var btn = card.querySelector('.make-live-btn');
+    if (btn) {
+      btn.disabled = isLive;
+      btn.textContent = isLive ? '● Live' : 'Make live';
+    }
+  });
+}
+
+function makeLive(key, btn) {
+  if (btn && btn.disabled) return;
+  if (btn) btn.disabled = true;
+  fetch('/api/admin/active-template', {
+    method: 'POST',
+    headers: {'Content-Type': 'application/json'},
+    body: JSON.stringify({gallery: 'admin', key: key})
+  })
+  .then(function(r) { return r.json(); })
+  .then(function(data) {
+    if (data.ok) {
+      setLiveBadge(data.active.admin);
+    } else {
+      if (btn) btn.disabled = false;
+      alert('Could not set live: ' + (data.error || 'unknown error'));
+    }
+  })
+  .catch(function(err) {
+    if (btn) btn.disabled = false;
+    alert('Request failed: ' + err.message);
+  });
+}
+
+// On load, fetch current active template and mark the live card
+fetch('/api/admin/active-template')
+  .then(function(r) { return r.json(); })
+  .then(function(data) { setLiveBadge(data && data.admin ? data.admin : null); }) // GET returns {admin,frontpage} directly (no .active wrapper — that's the POST shape)
+  .catch(function() { /* silently ignore if not authed */ });
+</script>
+</body></html>
diff --git a/public/admin/render.js b/public/admin/render.js
index 75332269..e09391f9 100644
--- a/public/admin/render.js
+++ b/public/admin/render.js
@@ -78,14 +78,27 @@
   }
 
   var opt = { credentials: 'same-origin' }; // keep Basic-Auth creds on the gated admin APIs
+  var jn = function (u) { return fetch(u, opt).then(function (r) { return r.ok ? r.json() : {}; }).catch(function () { return {}; }); };
+  // REAL admin endpoints (verified against the running server, TK-10356): audience / news / social videos
+  // / deal pulse / ticker. Each is normalized below into the exact shape build() consumes.
   Promise.allSettled([
-    fetch('/api/subscribers', opt).then(function (r) { return r.json(); }),
-    fetch('/api/news', opt).then(function (r) { return r.json(); }),
-    fetch('/api/videos', opt).then(function (r) { return r.json(); }),
-    fetch('/api/markets', opt).then(function (r) { return r.json(); }),
-    fetch('/api/ticker', opt).then(function (r) { return r.json(); }),
+    jn('/api/audience'),
+    jn('/api/news'),
+    jn('/api/social/videos'),
+    jn('/api/pulse'),
+    jn('/api/ticker'),
   ]).then(function (res) {
     var g = function (i) { return res[i].status === 'fulfilled' ? res[i].value : {}; };
-    build({ subs: g(0), news: g(1), vids: g(2), mk: g(3), tk: g(4) });
+    var aud = g(0);
+    var subs = {
+      count: (aud.stats && aud.stats.total) || 0,
+      unique: (aud.stats && (aud.stats.newsletter || aud.stats.total)) || 0,
+      items: (aud.contacts || []).slice(0, 20).map(function (c) { return { email: c.email, source: c.source || c.origin || '', at: c.created_at }; }),
+    };
+    var vraw = (g(2).items) || [];
+    var vcats = {}; vraw.forEach(function (v) { if (v.category) vcats[v.category] = 1; });
+    var vids = { items: vraw.map(function (v) { return { title: v.title, cat: v.category || '' }; }), cats: Object.keys(vcats) };
+    // mk = deal pulse (kpis.totalVolumeLabel/avgSizeLabel/dealCount); tk = ticker (10-YR etc.)
+    build({ subs: subs, news: g(1), vids: vids, mk: g(3), tk: g(4) });
   }).catch(function () { document.getElementById('app').innerHTML = '<p style="color:var(--sub);padding:40px">Admin data unavailable — retry.</p>'; });
 })();
diff --git a/public/versions/index.html b/public/versions/index.html
index 6148cd25..4201f64d 100644
--- a/public/versions/index.html
+++ b/public/versions/index.html
@@ -4,30 +4,159 @@
 .wrap{max-width:1180px;margin:0 auto;padding:0 22px}.hd{padding:44px 0 10px;text-align:center}
 .hd h1{font-family:'Playfair Display',serif;font-size:clamp(30px,5vw,52px)}.hd h1 span{color:#c8102e}
 .hd p{color:#aeb6bd;margin-top:10px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;padding:30px 0 60px}
-.vc{background:#121722;border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:22px;transition:.2s;display:block}
+.vc{background:#121722;border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:22px;transition:.2s;display:block;position:relative}
 .vc:hover{transform:translateY(-4px);border-color:#c8102e;box-shadow:0 18px 44px rgba(0,0,0,.4)}
+.vc.is-live{border-color:#c8102e;box-shadow:0 0 0 1px #c8102e}
 .sw{display:flex;height:40px;border-radius:6px;overflow:hidden;border:1px solid;margin-bottom:16px}.sw span{flex:1}
 .vc h3{font-family:'Playfair Display',serif;font-size:22px;color:#fff}.vc p{color:#aeb6bd;font-size:14px;margin:6px 0 12px}
-.vc .go{color:#c8102e;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.08em}</style></head><body>
+.card-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
+.vc .go{color:#c8102e;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;flex-shrink:0}
+.live-badge{display:none;align-items:center;gap:5px;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#2ea043;background:rgba(46,160,67,.12);border:1px solid rgba(46,160,67,.3);border-radius:4px;padding:3px 8px;flex-shrink:0}
+.live-badge::before{content:'●';font-size:9px}
+.vc.is-live .live-badge{display:flex}
+.preview-live-btn{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;background:transparent;border:1px solid rgba(255,255,255,.15);color:#aeb6bd;border-radius:5px;padding:5px 10px;cursor:pointer;transition:.15s;flex-shrink:0}
+.preview-live-btn:hover{border-color:#c8102e;color:#eef1f6;background:rgba(200,16,46,.08)}
+.vc.is-live .preview-live-btn{border-color:#c8102e;color:#c8102e;background:rgba(200,16,46,.08);cursor:default}
+.preview-live-btn:disabled{opacity:.5;cursor:not-allowed}
+.deploy-note{display:none;font-size:11px;color:#aeb6bd;margin-top:10px;padding:8px 10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:5px;line-height:1.5}
+.vc.is-live .deploy-note{display:block}
+.reset-bar{text-align:center;padding:0 0 40px}
+.reset-btn{background:transparent;border:1px solid rgba(255,255,255,.15);color:#aeb6bd;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;border-radius:6px;padding:9px 20px;cursor:pointer;transition:.15s}
+.reset-btn:hover{border-color:#c8102e;color:#eef1f6}
+.reset-btn:disabled{opacity:.4;cursor:not-allowed}
+</style></head><body>
 <header class="hd"><div class="wrap"><h1>REN<span>TV</span> · 10 Site Versions</h1><p>Ten complete directions for rentv.com — different layouts, not just colors. Open each; they all run on the live news feed. Pick the one that feels right.</p></div></header>
-<div class="wrap"><div class="grid"><a class="vc" href="/versions/wire">
-  <div class="sw" style="background:#fff;border-color:#e4e7ea"><span style="background:#c8102e"></span><span style="background:#14171a"></span><span style="background:#5b636b"></span><span style="background:#f5f6f8"></span></div>
-  <h3>The Wire</h3><p>Lead story + 3-column card river + right rail. The modern newsroom default.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/broadsheet">
-  <div class="sw" style="background:#faf7f2;border-color:#d6cfc4"><span style="background:#8b1a1a"></span><span style="background:#111"></span><span style="background:#555"></span><span style="background:#f0ece4"></span></div>
-  <h3>Broadsheet</h3><p>Dense two-column serif columns, small images — a printed newspaper on the web.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/terminal">
-  <div class="sw" style="background:#0a0f0a;border-color:#1e3a1e"><span style="background:#ff6600"></span><span style="background:#e8ffe8"></span><span style="background:#7fbf7f"></span><span style="background:#111a11"></span></div>
-  <h3>Terminal</h3><p>Dark, data-first deal rows with category tags — the CRE data-desk look.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/magazine">
-  <div class="sw" style="background:#fdf8f3;border-color:#e0d4c8"><span style="background:#b84a1e"></span><span style="background:#1e1712"></span><span style="background:#6b5a4e"></span><span style="background:#f5ede3"></span></div>
-  <h3>Magazine</h3><p>One oversized hero feature + two secondaries. Editorial and image-led.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/digest">
-  <div class="sw" style="background:#fdfde6;border-color:#d8d8b8"><span style="background:#2d5a27"></span><span style="background:#1a1a14"></span><span style="background:#5a5a40"></span><span style="background:#f5f5d0"></span></div>
-  <h3>Daily Digest</h3><p>Numbered compact headline list — reads like the morning email.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/grid">
-  <div class="sw" style="background:#fff;border-color:#e0e4eb"><span style="background:#1a3a6b"></span><span style="background:#0f1923"></span><span style="background:#5a6474"></span><span style="background:#f2f4f8"></span></div>
-  <h3>Photo Grid</h3><p>Uniform image-forward card grid. Every story equal, visual-first.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/split">
-  <div class="sw" style="background:#fff1e5;border-color:#d4c9b8"><span style="background:#00538b"></span><span style="background:#1a1a1a"></span><span style="background:#5c5c5c"></span><span style="background:#fde8d1"></span></div>
-  <h3>Split Desk</h3><p>Headlines column on the left, Most-Read + Markets rail on the right. No big hero.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/dashboard">
-  <div class="sw" style="background:#0d1b2a;border-color:#1e2d40"><span style="background:#c9a84c"></span><span style="background:#f0f2f5"></span><span style="background:#8fa3b8"></span><span style="background:#132336"></span></div>
-  <h3>Market Dashboard</h3><p>Metrics band on top, then the news list beside live market cards.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/minimal">
-  <div class="sw" style="background:#f8f9fb;border-color:#dce0e6"><span style="background:#3a5a8c"></span><span style="background:#1c2330"></span><span style="background:#586070"></span><span style="background:#edf0f4"></span></div>
-  <h3>Minimal</h3><p>Single centered column, generous whitespace, quiet typography.</p><span class="go">Open version →</span></a><a class="vc" href="/versions/feed">
-  <div class="sw" style="background:#181c22;border-color:#2e3540"><span style="background:#e82535"></span><span style="background:#f2f2f2"></span><span style="background:#9aa0a8"></span><span style="background:#22282f"></span></div>
-  <h3>Big Feed</h3><p>One column of large-image stories you scroll — mobile-native, bold.</p><span class="go">Open version →</span></a></div></div></body></html>
\ No newline at end of file
+<div class="wrap"><div class="grid" id="gallery">
+  <div class="vc" data-key="wire">
+    <div class="sw" style="background:#fff;border-color:#e4e7ea"><span style="background:#c8102e"></span><span style="background:#14171a"></span><span style="background:#5b636b"></span><span style="background:#f5f6f8"></span></div>
+    <h3>The Wire</h3><p>Lead story + 3-column card river + right rail. The modern newsroom default.</p>
+    <div class="card-foot"><a class="go" href="/versions/wire">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('wire',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="broadsheet">
+    <div class="sw" style="background:#faf7f2;border-color:#d6cfc4"><span style="background:#8b1a1a"></span><span style="background:#111"></span><span style="background:#555"></span><span style="background:#f0ece4"></span></div>
+    <h3>Broadsheet</h3><p>Dense two-column serif columns, small images — a printed newspaper on the web.</p>
+    <div class="card-foot"><a class="go" href="/versions/broadsheet">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('broadsheet',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="terminal">
+    <div class="sw" style="background:#0a0f0a;border-color:#1e3a1e"><span style="background:#ff6600"></span><span style="background:#e8ffe8"></span><span style="background:#7fbf7f"></span><span style="background:#111a11"></span></div>
+    <h3>Terminal</h3><p>Dark, data-first deal rows with category tags — the CRE data-desk look.</p>
+    <div class="card-foot"><a class="go" href="/versions/terminal">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('terminal',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="magazine">
+    <div class="sw" style="background:#fdf8f3;border-color:#e0d4c8"><span style="background:#b84a1e"></span><span style="background:#1e1712"></span><span style="background:#6b5a4e"></span><span style="background:#f5ede3"></span></div>
+    <h3>Magazine</h3><p>One oversized hero feature + two secondaries. Editorial and image-led.</p>
+    <div class="card-foot"><a class="go" href="/versions/magazine">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('magazine',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="digest">
+    <div class="sw" style="background:#fdfde6;border-color:#d8d8b8"><span style="background:#2d5a27"></span><span style="background:#1a1a14"></span><span style="background:#5a5a40"></span><span style="background:#f5f5d0"></span></div>
+    <h3>Daily Digest</h3><p>Numbered compact headline list — reads like the morning email.</p>
+    <div class="card-foot"><a class="go" href="/versions/digest">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('digest',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="grid">
+    <div class="sw" style="background:#fff;border-color:#e0e4eb"><span style="background:#1a3a6b"></span><span style="background:#0f1923"></span><span style="background:#5a6474"></span><span style="background:#f2f4f8"></span></div>
+    <h3>Photo Grid</h3><p>Uniform image-forward card grid. Every story equal, visual-first.</p>
+    <div class="card-foot"><a class="go" href="/versions/grid">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('grid',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="split">
+    <div class="sw" style="background:#fff1e5;border-color:#d4c9b8"><span style="background:#00538b"></span><span style="background:#1a1a1a"></span><span style="background:#5c5c5c"></span><span style="background:#fde8d1"></span></div>
+    <h3>Split Desk</h3><p>Headlines column on the left, Most-Read + Markets rail on the right. No big hero.</p>
+    <div class="card-foot"><a class="go" href="/versions/split">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('split',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="dashboard">
+    <div class="sw" style="background:#0d1b2a;border-color:#1e2d40"><span style="background:#c9a84c"></span><span style="background:#f0f2f5"></span><span style="background:#8fa3b8"></span><span style="background:#132336"></span></div>
+    <h3>Market Dashboard</h3><p>Metrics band on top, then the news list beside live market cards.</p>
+    <div class="card-foot"><a class="go" href="/versions/dashboard">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('dashboard',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="minimal">
+    <div class="sw" style="background:#f8f9fb;border-color:#dce0e6"><span style="background:#3a5a8c"></span><span style="background:#1c2330"></span><span style="background:#586070"></span><span style="background:#edf0f4"></span></div>
+    <h3>Minimal</h3><p>Single centered column, generous whitespace, quiet typography.</p>
+    <div class="card-foot"><a class="go" href="/versions/minimal">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('minimal',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+  <div class="vc" data-key="feed">
+    <div class="sw" style="background:#181c22;border-color:#2e3540"><span style="background:#e82535"></span><span style="background:#f2f2f2"></span><span style="background:#9aa0a8"></span><span style="background:#22282f"></span></div>
+    <h3>Big Feed</h3><p>One column of large-image stories you scroll — mobile-native, bold.</p>
+    <div class="card-foot"><a class="go" href="/versions/feed">Open version →</a><span class="live-badge">LIVE</span><button class="preview-live-btn" onclick="makeLive('feed',this)">Preview live here</button></div>
+    <div class="deploy-note">Instance updated. Publishing to the <strong>public site</strong> is a separate gated deploy — this change is active on this instance only.</div>
+  </div>
+</div>
+<div class="reset-bar">
+  <button class="reset-btn" id="resetBtn" onclick="resetDefault(this)">Reset to default front page</button>
+</div>
+</div>
+<script>
+function setLiveBadge(key) {
+  document.querySelectorAll('#gallery .vc').forEach(function(card) {
+    var isLive = card.dataset.key === key;
+    card.classList.toggle('is-live', isLive);
+    var btn = card.querySelector('.preview-live-btn');
+    if (btn) {
+      btn.disabled = isLive;
+      btn.textContent = isLive ? '● Live here' : 'Preview live here';
+    }
+  });
+  // If key is 'default' or null, no card is lit — that's correct
+  var resetBtn = document.getElementById('resetBtn');
+  if (resetBtn) resetBtn.disabled = (key === 'default' || !key);
+}
+
+function makeLive(key, btn) {
+  if (btn && btn.disabled) return;
+  if (btn) btn.disabled = true;
+  fetch('/api/admin/active-template', {
+    method: 'POST',
+    headers: {'Content-Type': 'application/json'},
+    body: JSON.stringify({gallery: 'frontpage', key: key})
+  })
+  .then(function(r) { return r.json(); })
+  .then(function(data) {
+    if (data.ok) {
+      setLiveBadge(data.active.frontpage);
+    } else {
+      if (btn) btn.disabled = false;
+      alert('Could not set live: ' + (data.error || 'unknown error'));
+    }
+  })
+  .catch(function(err) {
+    if (btn) btn.disabled = false;
+    alert('Request failed: ' + err.message);
+  });
+}
+
+function resetDefault(btn) {
+  if (btn) btn.disabled = true;
+  fetch('/api/admin/active-template', {
+    method: 'POST',
+    headers: {'Content-Type': 'application/json'},
+    body: JSON.stringify({gallery: 'frontpage', key: 'default'})
+  })
+  .then(function(r) { return r.json(); })
+  .then(function(data) {
+    if (data.ok) {
+      setLiveBadge(data.active.frontpage);
+    } else {
+      if (btn) btn.disabled = false;
+      alert('Reset failed: ' + (data.error || 'unknown error'));
+    }
+  })
+  .catch(function(err) {
+    if (btn) btn.disabled = false;
+    alert('Request failed: ' + err.message);
+  });
+}
+
+// On load, fetch current active template and mark the live card
+fetch('/api/admin/active-template')
+  .then(function(r) { return r.json(); })
+  .then(function(data) { setLiveBadge(data ? data.frontpage : null); }) // GET returns {admin,frontpage} directly (no .active wrapper — that's the POST shape)
+  .catch(function() { /* silently ignore if not authed */ });
+</script>
+</body></html>
diff --git a/public/versions/render.js b/public/versions/render.js
index 19a86995..14c72a31 100644
--- a/public/versions/render.js
+++ b/public/versions/render.js
@@ -1,5 +1,7 @@
 /* rentv-v1 version renderer — one file, 10 distinct homepage layouts. Reads
-   document.body.dataset.ver and renders that layout from the live /api/news feed. */
+   document.body.dataset.ver and renders that layout from the live /api/news feed.
+   Market stats in dashboard/terminal are populated from /api/pulse (admin-only);
+   on 403 (public viewer) the stat strip is hidden gracefully. */
 (function () {
   var ver = document.body.dataset.ver || 'wire';
   var esc = function (s) { return (s || '').replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); };
@@ -53,12 +55,28 @@
         '</div><aside><h3>Most Read</h3><ul class="mr">' + a.slice(0, 8).map(function (x) { return '<li><a href="' + href(x) + '">' + esc(x.title) + '</a></li>'; }).join('') + '</ul></aside></div>';
     },
     // 8. DASHBOARD — metrics band + list + market cards
-    dashboard: function (a) {
+    // Market cards sidebar populated from /api/pulse (admin-only).
+    // On 403 (public viewer) the stat strip is hidden; news list still renders.
+    dashboard: function (a, pulse) {
       css('.mb{display:flex;flex-wrap:wrap;gap:30px;padding:18px 20px;background:var(--wa);border:1px solid var(--ln);border-radius:10px;margin-bottom:24px}.mb .v{font-family:var(--se);font-size:30px;color:var(--a)}.mb .l{font-size:11px;text-transform:uppercase;color:var(--sub)}.db{display:grid;grid-template-columns:1fr 300px;gap:30px}.db .it{padding:12px 0;border-bottom:1px solid var(--ln)}.db h4{font-size:17px}.db .c{font-size:11px;font-weight:800;text-transform:uppercase;color:var(--a)}.mc{background:var(--wa);border:1px solid var(--ln);border-radius:8px;padding:14px 16px;margin-bottom:12px}.mc .n{font-family:var(--se);font-size:22px}@media(max-width:820px){.db{grid-template-columns:1fr}}');
       var band = [['3','Web properties'],['1998','Est.'],['600+','Conf. audience'],['Daily','Newsletter']];
+      var sidebar = '';
+      if (pulse) {
+        // Live data from /api/pulse — map to closest available metrics
+        var kpis = pulse.kpis || {};
+        var cards = [
+          [esc(kpis.totalVolumeLabel || '—'), 'Total Deal Volume'],
+          [esc(kpis.avgSizeLabel || '—'), 'Avg Deal Size'],
+          [String(kpis.dealCount || '—'), 'Deals Tracked']
+        ];
+        sidebar = cards.map(function (c) {
+          return '<div class="mc"><div class="l" style="font-size:11px;text-transform:uppercase;color:var(--sub)">' + c[1] + '</div><div class="n">' + c[0] + '</div></div>';
+        }).join('');
+      }
+      // If no pulse (public 403) sidebar stays empty — column collapses gracefully
       return '<div class="mb">' + band.map(function (b) { return '<div><div class="v">' + b[0] + '</div><div class="l">' + b[1] + '</div></div>'; }).join('') + '</div>' +
         '<div class="db"><div>' + a.slice(0, 14).map(function (x) { return '<a class="it" href="' + href(x) + '"><span class="c">' + esc(x.cat) + '</span><h4>' + esc(x.title) + '</h4></a>'; }).join('') +
-        '</div><aside><div class="mc"><div class="l" style="font-size:11px;text-transform:uppercase;color:var(--sub)">LA Office Vacancy</div><div class="n">23.1%</div></div><div class="mc"><div class="l" style="font-size:11px;text-transform:uppercase;color:var(--sub)">IE Industrial Cap</div><div class="n">5.2%</div></div><div class="mc"><div class="l" style="font-size:11px;text-transform:uppercase;color:var(--sub)">10-YR Treasury</div><div class="n">4.63%</div></div></aside></div>';
+        '</div>' + (sidebar ? '<aside>' + sidebar + '</aside>' : '') + '</div>';
     },
     // 9. MINIMAL — single centered column, whitespace
     minimal: function (a) {
@@ -72,10 +90,27 @@
     },
   };
 
+  // Fetch /api/news first (always succeeds on public), then /api/pulse (admin-only).
+  // /api/pulse 403 on public is handled gracefully — news still renders, stats hidden.
   fetch('/api/news').then(function (r) { return r.json(); }).then(function (d) {
     var items = (d.items || []);
     var app = document.getElementById('app');
     if (!items.length) { app.innerHTML = '<div class="load">Live feed is refreshing — check back shortly.</div>'; return; }
-    app.innerHTML = (L[ver] || L.wire)(items);
+
+    // Layouts that use pulse stats; others render immediately without it
+    var needsPulse = (ver === 'dashboard');
+    if (needsPulse) {
+      fetch('/api/pulse').then(function (pr) {
+        if (!pr.ok) { throw new Error('no-pulse'); }
+        return pr.json();
+      }).then(function (pulse) {
+        app.innerHTML = L.dashboard(items, pulse);
+      }).catch(function () {
+        // 403 (public) or any error — render dashboard without stat sidebar
+        app.innerHTML = L.dashboard(items, null);
+      });
+    } else {
+      app.innerHTML = (L[ver] || L.wire)(items);
+    }
   }).catch(function () { document.getElementById('app').innerHTML = '<div class="load">Live feed temporarily unavailable — retrying…</div>'; });
 })();

← 673bc25f rentv: select-to-live template spine — persist active admin/  ·  back to Rentv  ·  auto-data-snapshot: 2026-08-07T14:22:04 (8 data files) — dat 2104592e →