[object Object]

← back to Rentv

Add Front End <-> Backend toggle: site-wide injected widget + /backend admin hub

d2865684c7619ef133399c7157089c5546dd412e · 2026-07-28 11:27:58 -0700 · Steve Abrams

- /toggle.js: fixed pill reads /api/me, shows Front End|Backend for admin only (self-hides for user)
- /backend: admin-only hub dashboard linking Desk/Audience/Admin/Versions/Consulting/Press
- sendPage() injects toggle before </body> on all explicit page routes + a static HTML injector
  covers index/direct .html; non-HTML assets pass through untouched
- verified: injected on all pages for admin, /backend admin=200/user=403, theme.js untouched

TK-10013

Files touched

Diff

commit d2865684c7619ef133399c7157089c5546dd412e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 11:27:58 2026 -0700

    Add Front End <-> Backend toggle: site-wide injected widget + /backend admin hub
    
    - /toggle.js: fixed pill reads /api/me, shows Front End|Backend for admin only (self-hides for user)
    - /backend: admin-only hub dashboard linking Desk/Audience/Admin/Versions/Consulting/Press
    - sendPage() injects toggle before </body> on all explicit page routes + a static HTML injector
      covers index/direct .html; non-HTML assets pass through untouched
    - verified: injected on all pages for admin, /backend admin=200/user=403, theme.js untouched
    
    TK-10013
---
 public/backend.html | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/toggle.js    | 37 +++++++++++++++++++++++++++++++++++
 server.js           | 54 ++++++++++++++++++++++++++++++++++++++++------------
 3 files changed, 134 insertions(+), 12 deletions(-)

diff --git a/public/backend.html b/public/backend.html
new file mode 100644
index 00000000..4787dceb
--- /dev/null
+++ b/public/backend.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>RENTV — Backend</title>
+<style>
+  :root { --ink:#0d1b2a; --muted:#5b6b7b; --line:#e4e9ef; --bg:#f6f8fb; --accent:#0a6cff; }
+  * { box-sizing:border-box; }
+  body { margin:0; font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; color:var(--ink); background:var(--bg); }
+  header { padding:26px 32px; border-bottom:1px solid var(--line); background:#fff; display:flex; align-items:baseline; gap:14px; }
+  header h1 { margin:0; font-size:22px; letter-spacing:.2px; }
+  header .tag { font-size:11px; text-transform:uppercase; letter-spacing:.12em; color:#fff; background:var(--ink); padding:3px 9px; border-radius:999px; }
+  header a.front { margin-left:auto; font-size:13px; color:var(--accent); text-decoration:none; font-weight:600; }
+  main { max-width:1080px; margin:0 auto; padding:32px; }
+  p.lede { color:var(--muted); max-width:640px; margin:0 0 26px; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:18px; }
+  a.card { display:block; background:#fff; border:1px solid var(--line); border-radius:14px; padding:20px 22px; text-decoration:none; color:inherit; transition:border-color .15s, transform .15s, box-shadow .15s; }
+  a.card:hover { border-color:var(--accent); transform:translateY(-2px); box-shadow:0 8px 24px rgba(13,27,42,.08); }
+  a.card .ico { font-size:26px; }
+  a.card b { display:block; margin:10px 0 4px; font-size:16px; }
+  a.card span { color:var(--muted); font-size:13px; }
+  .sect { font-size:12px; text-transform:uppercase; letter-spacing:.1em; color:var(--muted); margin:32px 0 14px; }
+</style>
+</head>
+<body>
+<header>
+  <h1>RENTV Backend</h1><span class="tag">Admin</span>
+  <a class="front" href="/">🌐 Front End →</a>
+</header>
+<main>
+  <p class="lede">Internal operations for RENTV — the CRE intelligence registry, audience/CRM,
+  dashboards, the site/admin design tool, the consulting deliverable, and press. Admin-only.</p>
+
+  <div class="sect">Intelligence &amp; CRM</div>
+  <div class="grid">
+    <a class="card" href="/desk"><div class="ico">🗂️</div><b>Broker &amp; Owner Desk</b><span>382k brokers · 34k firms · sublease registry</span></a>
+    <a class="card" href="/audience"><div class="ico">🎯</div><b>Audience &amp; CRM</b><span>Subscribers + sublease brokers, auto-segmented</span></a>
+    <a class="card" href="/desk-admin"><div class="ico">✍️</div><b>Desk Admin</b><span>Add / withdraw sublease inventory</span></a>
+  </div>
+
+  <div class="sect">Build &amp; Dashboards</div>
+  <div class="grid">
+    <a class="card" href="/admin"><div class="ico">📊</div><b>Admin Dashboards</b><span>10 admin-version dashboards over live data</span></a>
+    <a class="card" href="/versions"><div class="ico">🎨</div><b>Site Versions</b><span>10 front-page layout concepts</span></a>
+  </div>
+
+  <div class="sect">Growth &amp; Comms</div>
+  <div class="grid">
+    <a class="card" href="/consulting"><div class="ico">🚀</div><b>Consulting</b><span>Growth deliverable · concepts · intake · command center</span></a>
+    <a class="card" href="/press"><div class="ico">🎬</div><b>Press &amp; Promos</b><span>Recap films + launch captures</span></a>
+  </div>
+</main>
+</body>
+</html>
diff --git a/public/toggle.js b/public/toggle.js
new file mode 100644
index 00000000..cd37fc79
--- /dev/null
+++ b/public/toggle.js
@@ -0,0 +1,37 @@
+/* RENTV Front End ⇄ Backend toggle.
+ * Injected site-wide by server.js. Reads /api/me; renders ONLY for admin.
+ * user-tier logins see nothing (they have no backend). */
+(function () {
+  if (window.__rentvToggle) return; window.__rentvToggle = 1;
+  // Paths that count as "backend" for active-state highlighting.
+  var BACKEND = /^\/(backend|desk|desk-admin|admin|versions|audience|consulting|press)(\/|$|\.html)/i;
+
+  fetch('/api/me', { credentials: 'same-origin' })
+    .then(function (r) { return r.ok ? r.json() : { role: 'user' }; })
+    .then(function (m) {
+      if (!m || m.role !== 'admin') return; // only admins get the toggle
+      var onBackend = BACKEND.test(location.pathname);
+
+      var css = document.createElement('style');
+      css.textContent =
+        '#rentv-toggle{position:fixed;left:16px;bottom:16px;z-index:2147483000;' +
+        'display:flex;align-items:center;gap:0;background:#0d1b2a;border:1px solid #26384c;' +
+        'border-radius:999px;padding:4px;box-shadow:0 6px 22px rgba(0,0,0,.28);' +
+        'font:600 12.5px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}' +
+        '#rentv-toggle a{display:flex;align-items:center;gap:6px;padding:7px 13px;border-radius:999px;' +
+        'color:#9fb2c6;text-decoration:none;white-space:nowrap;transition:background .15s,color .15s;}' +
+        '#rentv-toggle a:hover{color:#e8eef5;}' +
+        '#rentv-toggle a.on{background:#0a6cff;color:#fff;box-shadow:0 1px 4px rgba(10,108,255,.4);}' +
+        '#rentv-toggle .lbl{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#5b6b7b;padding:0 8px 0 6px;}';
+      document.head.appendChild(css);
+
+      var el = document.createElement('div');
+      el.id = 'rentv-toggle';
+      el.innerHTML =
+        '<span class="lbl">View</span>' +
+        '<a href="/" class="' + (onBackend ? '' : 'on') + '" title="Public front-page site">🌐 Front End</a>' +
+        '<a href="/backend" class="' + (onBackend ? 'on' : '') + '" title="Internal admin backend">🛠 Backend</a>';
+      document.body.appendChild(el);
+    })
+    .catch(function () { /* unauth / offline — no toggle */ });
+})();
diff --git a/server.js b/server.js
index b86249d8..58421bbf 100644
--- a/server.js
+++ b/server.js
@@ -48,6 +48,19 @@ function adminOnly(req, res, next) {
   if (req.role === 'admin') return next();
   return res.status(403).send('Admin only — this area requires an admin login.');
 }
+// sendPage(): serve an HTML file with the Front End ⇄ Backend toggle injected before
+// </body>. Used by every explicit page route AND the static HTML injector below, so the
+// toggle is present site-wide from a single include (the widget self-hides for user-tier).
+const TOGGLE_TAG = '<script src="/toggle.js" defer></script>';
+function sendPage(res, absFile) {
+  let html;
+  try { html = fs.readFileSync(absFile, 'utf8'); }
+  catch { return res.status(404).send('not found'); }
+  if (html.indexOf('/toggle.js') === -1) {
+    html = html.indexOf('</body>') !== -1 ? html.replace('</body>', TOGGLE_TAG + '</body>') : html + TOGGLE_TAG;
+  }
+  res.type('html').send(html);
+}
 // Expose the caller's role to the front end so shells can hide/show internal nav.
 app.get('/api/me', (req, res) => res.json({ role: req.role || 'user' }));
 
@@ -259,19 +272,21 @@ app.get('/api/article/:id', async (req, res) => {
 //    live feed, one consistent model site-wide; no thin static section pages). ──
 const SECTIONS = ['financing', 'leases', 'development', 'retail', 'multifamily', 'industrial', 'sales'];
 SECTIONS.forEach(s => app.get('/' + s, (_q, r) => r.redirect(301, '/?cat=' + s.charAt(0).toUpperCase() + s.slice(1))));
-app.get('/deals', (_q, r) => r.sendFile(path.join(PUB, 'deals.html')));
-app.get('/news/:id', (_q, r) => r.sendFile(path.join(PUB, 'article.html')));
+app.get('/deals', (_q, r) => sendPage(r, path.join(PUB, 'deals.html')));
+app.get('/news/:id', (_q, r) => sendPage(r, path.join(PUB, 'article.html')));
 // The REview — the folded-in video platform, served natively in the RENTV shell
-app.get('/review', (_q, r) => r.sendFile(path.join(PUB, 'review.html')));
+app.get('/review', (_q, r) => sendPage(r, path.join(PUB, 'review.html')));
+// Backend hub — admin-only landing linking every internal tool (the "Backend" toggle target)
+app.get('/backend', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'backend.html')));
 // 10 site VERSIONS — internal design/layout tool → admin-only
-app.get('/versions', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'versions', 'index.html')));
+app.get('/versions', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'versions', 'index.html')));
 // 10 ADMIN versions — dashboards over the real internal data → admin-only
-app.get('/admin', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'admin', 'index.html')));
+app.get('/admin', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'admin', 'index.html')));
 // Broker & Owner Intelligence Desk shell → admin-only (its data APIs are gated below too)
-app.get('/desk', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'desk.html')));
+app.get('/desk', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'desk.html')));
 // Blog — public index of original RENTV articles + the single-post reader
-app.get('/blog', (_q, r) => r.sendFile(path.join(PUB, 'blog.html')));
-app.get('/post/:id', (_q, r) => r.sendFile(path.join(PUB, 'post.html')));
+app.get('/blog', (_q, r) => sendPage(r, path.join(PUB, 'blog.html')));
+app.get('/post/:id', (_q, r) => sendPage(r, path.join(PUB, 'post.html')));
 
 // ── Broker & Owner Intelligence Desk (/desk): proxy the usrealestate CA registry ──
 //    Keeps the desk on the FULL live registry (382k brokers / 34k firms) without
@@ -308,7 +323,7 @@ async function usreWrite(method, upstream, req, res) {
 }
 // Sublease writes + the desk-admin console are admin-only (role-resolved above). Served from
 // OUTSIDE public/ so express.static can't bypass the gate.
-app.get('/desk-admin', adminOnly, (_req, res) => res.sendFile(path.join(__dirname, 'admin', 'desk-admin.html')));
+app.get('/desk-admin', adminOnly, (_req, res) => sendPage(res, path.join(__dirname, 'admin', 'desk-admin.html')));
 app.post('/api/admin/sublease', adminOnly, (req, res) => usreWrite('POST', '/api/admin/sublease', req, res));
 app.delete('/api/admin/sublease/:id', adminOnly, (req, res) => {
   const id = String(req.params.id).replace(/[^\d]/g, '');
@@ -427,7 +442,7 @@ app.get('/api/audience', adminOnly, async (_q, res) => {
   res.set('Cache-Control', 'private, max-age=60');
   res.json(buildAudience(subs, subleaseRows));
 });
-app.get('/audience', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'audience.html')));
+app.get('/audience', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'audience.html')));
 
 // ── Global site search — one box across every content surface. ──
 // Result hrefs are ALWAYS internal (or external YouTube for videos); we never
@@ -557,7 +572,7 @@ app.get('/deals.xml', (req, res) => {
 // ── Folded-in admin-only areas ──────────────────────────────────────────────
 // /consulting — the former consulting-rentv-com deliverable (concept versions,
 //   portal, slideshow), now an admin-only section of the unified site.
-app.get('/consulting', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'consulting', 'portal.html')));
+app.get('/consulting', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'consulting', 'portal.html')));
 // /consulting dynamic API — ported from consulting-rentv-com so the deliverable's intake
 // form + admin buckets work inside the unified app. Data lives in data/consulting/.
 // All admin-only (the whole /consulting area is admin per the fold-in decision).
@@ -591,7 +606,7 @@ app.post('/consulting/api/admin/:bucket', adminOnly, (req, res) => {
   res.json({ ok: true });
 });
 // /press — the rentv-promos recap video + launch captures, admin/press only.
-app.get('/press', adminOnly, (_q, r) => r.sendFile(path.join(PUB, 'press', 'index.html')));
+app.get('/press', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'press', 'index.html')));
 
 // ── Defense-in-depth static guard ───────────────────────────────────────────
 // The data APIs above are already admin-only; this additionally blocks user-tier
@@ -603,5 +618,20 @@ app.use((req, res, next) => {
   if (INTERNAL_STATIC.test(req.path)) return res.status(403).send('Admin only — this area requires an admin login.');
   next();
 });
+// Inject the Front End ⇄ Backend toggle into HTML pages that express.static would serve
+// (index + direct *.html + extensionless→.html), mirroring its extensions:['html'] resolution.
+// Non-HTML assets (js/css/img/json/mp4) fall straight through to express.static untouched.
+app.use((req, res, next) => {
+  if (req.method !== 'GET') return next();
+  let rel = decodeURIComponent(req.path);
+  if (rel.endsWith('/')) rel += 'index.html';
+  if (!/\.html?$/i.test(rel)) {
+    if (fs.existsSync(path.join(PUB, rel + '.html'))) rel += '.html';
+    else return next();
+  }
+  const abs = path.normalize(path.join(PUB, rel));
+  if (!abs.startsWith(PUB) || !/\.html$/i.test(abs) || !fs.existsSync(abs)) return next();
+  return sendPage(res, abs);
+});
 app.use(express.static(PUB, { extensions: ['html'] }));
 app.listen(PORT, () => console.log('rentv (unified site — user/admin tiers, live data) on ' + PORT));

← 6660ab36 Fold /consulting dynamic API inline (intake + admin buckets)  ·  back to Rentv  ·  Admin Video Library: gather all RENTV films/walkthroughs/rec 9df21f11 →