← 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
M public/admin/index.htmlM public/admin/render.jsM public/versions/index.htmlM public/versions/render.js
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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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 →