[object Object]

← back to Rentv

rentv: select-to-live template spine — persist active admin/front-page layout, /api/admin/active-template, serve-active routing (TK-10356)

673bc25fa8a5f11a2e0d53cf6a6dd9f5892d112a · 2026-08-07 14:08:33 -0700 · Steve

Files touched

Diff

commit 673bc25fa8a5f11a2e0d53cf6a6dd9f5892d112a
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 14:08:33 2026 -0700

    rentv: select-to-live template spine — persist active admin/front-page layout, /api/admin/active-template, serve-active routing (TK-10356)
---
 .deploy.conf |  4 +++-
 server.js    | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 61 insertions(+), 4 deletions(-)

diff --git a/.deploy.conf b/.deploy.conf
index 8e2de9aa..6d1cc417 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -11,7 +11,9 @@ BUILD_CMD=""
 # deals-registry.json accumulates on prod (the news cron chains pull-deals → registry, TK-10246) — never clobber prod's growing deal history + change log with the local dev copy.
 # deals.json / news.json / markets.json / search-index-meta.json are ALL prod-cron runtime output (pull-cre-news → pull-deals → build-index); prod's live crawl owns them — never overwrite with the local dev copy (added 2026-08-06 when the cycle 28-33 parser fixes deploy: code-only, prod regenerates the data on its next cron).
 # public/img/news = prod-cron + live article-proxy runtime output (localize.cjs downloads each rentv.com photo here on prod). With rsync --delete, a deploy was WIPING prod's downloaded images (local dir is gitignored/stale) → every front-page image 404'd blank (TK-10284, 2026-08-06). Prod owns this dir — never clobber it.
-RSYNC_EXTRA_EXCLUDES="data/subscribers.jsonl data/posts.json data/forsale.json data/vimeo-library.json data/social-drafts.json data/news-archive.json data/deals-registry.json data/deals.json data/news.json data/markets.json data/search-index-meta.json data/search-index.jsonl data/ticker.json public/img/news public/audio"
+# data/active-templates.json = the LIVE select-to-live choice (TK-10356), set by admins on prod via
+# POST /api/admin/active-template — prod owns it; never clobber it with the local dev copy on deploy.
+RSYNC_EXTRA_EXCLUDES="data/subscribers.jsonl data/posts.json data/forsale.json data/vimeo-library.json data/social-drafts.json data/news-archive.json data/deals-registry.json data/deals.json data/news.json data/markets.json data/search-index-meta.json data/search-index.jsonl data/ticker.json data/active-templates.json public/img/news public/audio"
 # NOTE (2026-07-28): renamed from rentv-v1 -> rentv (unified RENTV app). The prod
 # dir /root/public-projects/rentv-v1 must be renamed to /root/public-projects/rentv
 # and pm2 re-registered as `rentv` as part of the Steve-gated cutover — see
diff --git a/server.js b/server.js
index 665ca8f8..7d96fed0 100644
--- a/server.js
+++ b/server.js
@@ -1509,10 +1509,65 @@ app.get('/backend', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'backend.ht
 // PLUS a manual override layer (add / edit / hide) served by /api/closings. Admin-only:
 // gated here, hidden from user-tier nav, and treated as an INTERNAL_PAGE. Not public.
 app.get('/closings', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'closings.html')));
-// 10 site VERSIONS — internal design/layout tool → admin-only
+// ── Select-to-live template state (TK-10356) ── the persisted "which layout is live" choice for
+// the ADMIN dashboard (internal) and the FRONT-PAGE version (customer-facing). Lives in data/ so it
+// survives restarts; .deploy.conf excludes it so a deploy never clobbers prod's live selection.
+const ACTIVE_TPL_FILE = path.join(DATA, 'active-templates.json');
+const ADMIN_LAYOUTS = ['classic', 'sidebar', 'console', 'cards', 'table', 'analytics', 'split', 'kanban', 'minimal', 'command'];
+const FRONT_LAYOUTS = ['wire', 'broadsheet', 'terminal', 'magazine', 'digest', 'grid', 'split', 'dashboard', 'minimal', 'feed'];
+// frontpage sentinel 'default' = keep the current customer-facing public/index.html unchanged.
+const readActiveTpl = () => {
+  const d = readJSON('active-templates.json', {});
+  return {
+    admin: ADMIN_LAYOUTS.includes(d.admin) ? d.admin : 'classic',
+    frontpage: (d.frontpage === 'default' || FRONT_LAYOUTS.includes(d.frontpage)) ? d.frontpage : 'default',
+  };
+};
+// Atomic write (tmp+rename) so a concurrent reader never sees a half-written file — same failure
+// class the readJSON/asArr guards elsewhere in this file defend against.
+const writeActiveTpl = (next) => {
+  const merged = { ...readActiveTpl(), ...next, updated_at: new Date().toISOString() };
+  const tmp = ACTIVE_TPL_FILE + '.tmp';
+  fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
+  fs.renameSync(tmp, ACTIVE_TPL_FILE);
+  return merged;
+};
+
+// Front page: honor the active front-page version (select-to-live). 'default' = the current public
+// index.html (unchanged). A chosen version renders that layout from the live feed. Declared BEFORE
+// express.static so it wins. Publishing a version to PROD is a gated deploy (see the POST below).
+app.get('/', (req, res) => {
+  const fp = readActiveTpl().frontpage;
+  if (fp && fp !== 'default') return sendPage(res, path.join(PUB, 'versions', fp + '.html'));
+  return sendPage(res, path.join(PUB, 'index.html'));
+});
+
+// 10 site VERSIONS — internal design/layout tool → admin-only. Gallery at /versions (+ /versions/gallery).
 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) => sendPage(r, path.join(PUB, 'admin', 'index.html')));
+app.get('/versions/gallery', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'versions', 'index.html')));
+// 10 ADMIN versions — dashboards over the real internal data → admin-only. Select-to-live (TK-10356):
+// /admin serves the ACTIVE dashboard; the pick-a-layout gallery moves to /admin/gallery. Individual
+// layouts stay browsable at /admin/<layout> via express.static.
+app.get('/admin', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'admin', readActiveTpl().admin + '.html')));
+app.get('/admin/gallery', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'admin', 'index.html')));
+
+// Select-to-live API (TK-10356). GET → current active choice; POST {gallery, key} → set it live.
+app.get('/api/admin/active-template', adminOnly, (_q, res) => res.json(readActiveTpl()));
+app.post('/api/admin/active-template', adminOnly, (req, res) => {
+  const gallery = String((req.body && req.body.gallery) || '');
+  const key = String((req.body && req.body.key) || '');
+  if (gallery === 'admin') {
+    if (!ADMIN_LAYOUTS.includes(key)) return res.status(400).json({ ok: false, error: 'unknown admin layout: ' + key });
+    return res.json({ ok: true, active: writeActiveTpl({ admin: key }), note: 'live admin now serves ' + key });
+  }
+  if (gallery === 'frontpage') {
+    if (key !== 'default' && !FRONT_LAYOUTS.includes(key)) return res.status(400).json({ ok: false, error: 'unknown front-page version: ' + key });
+    // Front-page is customer-facing: this flips / on THIS instance immediately; the public flip on
+    // prod (rentv.agentabrams.com) is a gated deploy, never auto-pushed.
+    return res.json({ ok: true, active: writeActiveTpl({ frontpage: key }), note: key === 'default' ? 'front page reset to default' : 'front page now renders ' + key + ' here (public flip = gated deploy)' });
+  }
+  return res.status(400).json({ ok: false, error: "gallery must be 'admin' or 'frontpage'" });
+});
 // Broker & Owner Intelligence Desk shell → admin-only (its data APIs are gated below too)
 app.get('/desk', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'desk.html')));
 // Summit Leads — build invite + sponsor-target lists off the registry (admin-only)

← fb593cab auto-data-snapshot: 2026-08-07T13:51:08 (7 data files) — dat  ·  back to Rentv  ·  rentv: wire all 10 admin dashboards to live data (audience/n daa67ba1 →