← back to Rentv 2026
public/admin/render.js
92 lines
/* rentv-v1 ADMIN — one file, 10 distinct admin-dashboard layouts over the real
gated data (subscribers, news feed, videos, markets). Reads body.dataset.ver. */
(function () {
var ver = document.body.dataset.ver || 'classic';
var esc = function (s) { return (s || '').replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
var css = function (t) { var s = document.createElement('style'); s.textContent = t; document.head.appendChild(s); };
var when = function (iso) { try { var m = Math.round((Date.now() - new Date(iso)) / 60000); return m < 2 ? 'just now' : m + ' min ago'; } catch (e) { return '—'; } };
function build(D) {
var subs = D.subs || {}, news = D.news || {}, vids = D.vids || {}, mk = D.mk || {}, tk = D.tk || {};
var subN = subs.count || 0, subU = subs.unique || 0, subList = (subs.items || []).slice(0, 12);
var nItems = news.items || [], vItems = vids.items || [], curve = mk.curve || [];
var KPI = [
['Subscribers', subU, subN + ' total signups'],
['Live stories', nItems.length, 'feed ' + when(news.fetched_at)],
['REview videos', vItems.length, (vids.cats || []).length + ' categories'],
['10-YR', (tk.ten_yr && tk.ten_yr.value) || (curve.find && (curve.find(function (c) { return /10/.test(c.label); }) || {}).value) || '—', 'treasury'],
];
var kpiCards = function (cls) { return KPI.map(function (k) { return '<div class="' + (cls || 'kpi') + '"><div class="v">' + esc('' + k[1]) + '</div><div class="l">' + esc(k[0]) + '</div><div class="s">' + esc(k[2]) + '</div></div>'; }).join(''); };
var subRows = function () { return subList.length ? subList.map(function (s) { return '<tr><td>' + esc(s.email) + '</td><td>' + esc(s.source || '') + '</td><td>' + esc((s.at || '').slice(0, 16).replace('T', ' ')) + '</td></tr>'; }).join('') : '<tr><td colspan="3" class="mut">No signups yet.</td></tr>'; };
var newsRows = function (n) { return nItems.slice(0, n || 10).map(function (x) { return '<tr><td class="c">' + esc(x.cat) + '</td><td>' + esc(x.title) + '</td><td class="mut">#' + esc(x.id) + '</td></tr>'; }).join(''); };
var vidList = function (n) { return vItems.slice(0, n || 8).map(function (v) { return '<li>' + esc(v.title) + ' <span class="mut">· ' + esc(v.cat || '') + '</span></li>'; }).join(''); };
var T = {
// 1 CLASSIC — KPI cards + two tables
classic: function () {
css('.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.kpi{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:18px}.kpi .v{font-size:32px;font-weight:800;color:var(--a)}.kpi .l{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);margin-top:4px}.kpi .s{font-size:12px;color:var(--sub);margin-top:2px}.tw{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:24px}.card{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:16px 18px}.card h3{font-size:14px;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px}table{width:100%;border-collapse:collapse;font-size:13px}td,th{text-align:left;padding:7px 6px;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-weight:700;font-size:11px;text-transform:uppercase}.mut{color:var(--sub)}@media(max-width:820px){.kpis,.tw{grid-template-columns:1fr}}');
return '<div class="kpis">' + kpiCards() + '</div><div class="tw"><div class="card"><h3>Recent subscribers</h3><table><tr><th>Email</th><th>Source</th><th>When</th></tr>' + subRows() + '</table></div><div class="card"><h3>Latest stories</h3><table>' + newsRows(10) + '</table></div></div>';
},
// 2 SIDEBAR
sidebar: function () {
css('.lay{display:grid;grid-template-columns:200px 1fr;gap:24px}.side{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:14px}.side a{display:block;padding:9px 10px;border-radius:6px;color:var(--sub);font-size:13px;font-weight:600}.side a.on{background:var(--a);color:#fff}.kpis{display:grid;grid-template-columns:1fr 1fr;gap:14px}.kpi{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:16px}.kpi .v{font-size:28px;font-weight:800;color:var(--a)}.kpi .l{font-size:11px;text-transform:uppercase;color:var(--sub)}.kpi .s{font-size:11px;color:var(--sub)}.card{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:16px;margin-top:16px}table{width:100%;border-collapse:collapse;font-size:13px}td,th{text-align:left;padding:6px;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-size:11px;font-weight:700;text-transform:uppercase}.mut{color:var(--sub)}@media(max-width:760px){.lay{grid-template-columns:1fr}}');
return '<div class="lay"><nav class="side"><a class="on">Overview</a><a>Subscribers</a><a>News feed</a><a>The REview</a><a>Markets</a><a>Settings</a></nav><div><div class="kpis">' + kpiCards() + '</div><div class="card"><h3 style="font-size:14px;text-transform:uppercase;margin-bottom:8px">Latest stories</h3><table>' + newsRows(8) + '</table></div></div></div>';
},
// 3 CONSOLE (dark ops)
console: function () {
css('body.ver-console{}.con{font-family:ui-monospace,monospace}.row{display:flex;gap:26px;flex-wrap:wrap;padding:14px 0;border-bottom:1px solid var(--ln)}.stat .v{font-size:26px;color:var(--a)}.stat .l{font-size:10px;text-transform:uppercase;color:var(--sub);letter-spacing:.1em}.log{margin-top:14px;font-size:12px}.log div{padding:5px 0;border-bottom:1px solid var(--ln)}.c{color:var(--a)}.mut{color:var(--sub)}');
return '<div class="con"><div class="row">' + KPI.map(function (k) { return '<div class="stat"><div class="v">' + esc('' + k[1]) + '</div><div class="l">' + esc(k[0]) + '</div></div>'; }).join('') + '</div><div class="log">' + nItems.slice(0, 16).map(function (x) { return '<div><span class="c">[' + esc(x.cat) + ']</span> ' + esc(x.title) + ' <span class="mut">#' + esc(x.id) + '</span></div>'; }).join('') + '</div></div>';
},
// 4 CARDS (module grid)
cards: function () {
css('.mods{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.mod{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:18px}.mod h3{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);margin-bottom:8px}.mod .big{font-size:34px;font-weight:800;color:var(--a)}.mod ul{list-style:none;font-size:13px}.mod li{padding:5px 0;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-weight:700}.mut{color:var(--sub)}');
return '<div class="mods"><div class="mod"><h3>Subscribers</h3><div class="big">' + subU + '</div><p class="mut">' + subN + ' total signups</p></div><div class="mod"><h3>Live feed</h3><div class="big">' + nItems.length + '</div><p class="mut">updated ' + when(news.fetched_at) + '</p></div><div class="mod"><h3>REview</h3><div class="big">' + vItems.length + '</div><p class="mut">videos</p></div><div class="mod"><h3>Latest videos</h3><ul>' + vidList(6) + '</ul></div><div class="mod" style="grid-column:span 2"><h3>Latest stories</h3><ul>' + nItems.slice(0, 8).map(function (x) { return '<li><span class="c">' + esc(x.cat) + '</span> · ' + esc(x.title) + '</li>'; }).join('') + '</ul></div></div>';
},
// 5 TABLE (dense spreadsheet)
table: function () {
css('.bar{display:flex;gap:24px;padding:10px 0 16px}.bar b{color:var(--a)}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;text-transform:uppercase;font-size:11px;letter-spacing:.06em;color:var(--sub);border-bottom:2px solid var(--a);padding:8px}td{padding:8px;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-weight:700;font-size:11px;text-transform:uppercase}.mut{color:var(--sub)}');
return '<div class="bar"><span>Subscribers <b>' + subU + '</b></span><span>Stories <b>' + nItems.length + '</b></span><span>Videos <b>' + vItems.length + '</b></span><span>Feed <b>' + when(news.fetched_at) + '</b></span></div><table><tr><th>Sector</th><th>Headline</th><th>Ref</th></tr>' + newsRows(20) + '</table>';
},
// 6 ANALYTICS (big numbers first)
analytics: function () {
css('.hero{text-align:center;padding:24px 0 8px}.hero .v{font-size:64px;font-weight:800;color:var(--a);line-height:1}.hero .l{font-size:13px;text-transform:uppercase;letter-spacing:.1em;color:var(--sub)}.strip{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.s{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:16px;text-align:center}.s .v{font-size:28px;font-weight:800;color:var(--a)}.s .l{font-size:11px;text-transform:uppercase;color:var(--sub)}.card{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:16px}table{width:100%;border-collapse:collapse;font-size:13px}td{padding:6px;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-size:11px;font-weight:700;text-transform:uppercase}');
return '<div class="hero"><div class="v">' + subU + '</div><div class="l">Newsletter subscribers</div></div><div class="strip"><div class="s"><div class="v">' + nItems.length + '</div><div class="l">Live stories</div></div><div class="s"><div class="v">' + vItems.length + '</div><div class="l">Videos</div></div><div class="s"><div class="v">' + subN + '</div><div class="l">Total signups</div></div></div><div class="card"><table>' + newsRows(10) + '</table></div>';
},
// 7 SPLIT (master-detail)
split: function () {
css('.sp{display:grid;grid-template-columns:1fr 300px;gap:24px}.list{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:8px}.list .it{padding:10px 12px;border-bottom:1px solid var(--ln);font-size:14px}.list .c{color:var(--a);font-size:11px;font-weight:700;text-transform:uppercase}.side .kpi{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:14px;margin-bottom:12px}.side .v{font-size:26px;font-weight:800;color:var(--a)}.side .l{font-size:11px;text-transform:uppercase;color:var(--sub)}@media(max-width:820px){.sp{grid-template-columns:1fr}}');
return '<div class="sp"><div class="list">' + nItems.slice(0, 14).map(function (x) { return '<div class="it"><span class="c">' + esc(x.cat) + '</span> ' + esc(x.title) + '</div>'; }).join('') + '</div><aside class="side">' + KPI.map(function (k) { return '<div class="kpi"><div class="v">' + esc('' + k[1]) + '</div><div class="l">' + esc(k[0]) + '</div></div>'; }).join('') + '</aside></div>';
},
// 8 KANBAN (by category)
kanban: function () {
var cats = {}; nItems.forEach(function (x) { (cats[x.cat] = cats[x.cat] || []).push(x); });
css('.kb{display:flex;gap:14px;overflow-x:auto;padding-bottom:8px}.col{min-width:220px;flex:1;background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:10px}.col h3{font-size:12px;text-transform:uppercase;color:var(--a);padding:4px 6px}.ct{background:var(--bg);border:1px solid var(--ln);border-radius:6px;padding:8px;margin:6px 0;font-size:12px}');
return '<div class="kb">' + Object.keys(cats).map(function (c) { return '<div class="col"><h3>' + esc(c) + ' · ' + cats[c].length + '</h3>' + cats[c].slice(0, 8).map(function (x) { return '<div class="ct">' + esc(x.title) + '</div>'; }).join('') + '</div>'; }).join('') + '</div>';
},
// 9 MINIMAL
minimal: function () {
css('.mn{max-width:640px;margin:0 auto}.mn .row{display:flex;justify-content:space-between;padding:14px 0;border-bottom:1px solid var(--ln)}.mn .row .v{font-weight:800;color:var(--a)}.mn h3{font-size:13px;text-transform:uppercase;color:var(--sub);margin:24px 0 6px}.mn .it{padding:8px 0;border-bottom:1px solid var(--ln);font-size:14px}');
return '<div class="mn"><div class="row"><span>Subscribers</span><span class="v">' + subU + '</span></div><div class="row"><span>Live stories</span><span class="v">' + nItems.length + '</span></div><div class="row"><span>Videos</span><span class="v">' + vItems.length + '</span></div><div class="row"><span>Feed updated</span><span class="v">' + when(news.fetched_at) + '</span></div><h3>Latest</h3>' + nItems.slice(0, 8).map(function (x) { return '<div class="it">' + esc(x.title) + '</div>'; }).join('') + '</div>';
},
// 10 COMMAND CENTER (multi-panel)
command: function () {
css('.cc{display:grid;grid-template-columns:repeat(6,1fr);grid-auto-rows:minmax(90px,auto);gap:12px}.p{background:var(--pan);border:1px solid var(--ln);border-radius:10px;padding:14px}.p h3{font-size:11px;text-transform:uppercase;color:var(--sub)}.p .v{font-size:30px;font-weight:800;color:var(--a)}.p.wide{grid-column:span 4}.p.tall{grid-row:span 3}.p ul,.p table{list-style:none;font-size:12px;width:100%;border-collapse:collapse}.p li,.p td{padding:5px 0;border-bottom:1px solid var(--ln)}.c{color:var(--a);font-weight:700}@media(max-width:820px){.cc{grid-template-columns:1fr 1fr}.p.wide{grid-column:span 2}}');
return '<div class="cc"><div class="p"><h3>Subs</h3><div class="v">' + subU + '</div></div><div class="p"><h3>Stories</h3><div class="v">' + nItems.length + '</div></div><div class="p wide"><h3>Latest stories</h3><table>' + newsRows(6) + '</table></div><div class="p"><h3>Videos</h3><div class="v">' + vItems.length + '</div></div><div class="p"><h3>Feed</h3><div class="v" style="font-size:16px">' + when(news.fetched_at) + '</div></div><div class="p wide tall"><h3>Recent subscribers</h3><table><tr><td><b>Email</b></td><td><b>Source</b></td></tr>' + subList.slice(0, 8).map(function (s) { return '<tr><td>' + esc(s.email) + '</td><td>' + esc(s.source || '') + '</td></tr>'; }).join('') + '</table></div><div class="p tall"><h3>REview</h3><ul>' + vidList(7) + '</ul></div></div>';
},
};
document.getElementById('app').innerHTML = (T[ver] || T.classic)();
}
var opt = { credentials: 'same-origin' }; // keep Basic-Auth creds on the gated admin APIs
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(); }),
]).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) });
}).catch(function () { document.getElementById('app').innerHTML = '<p style="color:var(--sub);padding:40px">Admin data unavailable — retry.</p>'; });
})();