[object Object]

← back to Marketing Command Center

Collapse 8 Preview panels into a single Insights hub (tabbed, single-pane swap); nav now 6 groups (v1.9.1)

7014784021a561f0b79e23a287ae796ba110b73c · 2026-08-12 14:01:31 -0700 · Steve

Files touched

Diff

commit 7014784021a561f0b79e23a287ae796ba110b73c
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 14:01:31 2026 -0700

    Collapse 8 Preview panels into a single Insights hub (tabbed, single-pane swap); nav now 6 groups (v1.9.1)
---
 modules/insights/index.js   | 11 ++++++++++
 modules/registry.js         |  1 +
 package.json                |  2 +-
 public/app.js               |  8 +++++--
 public/panels/insights.html | 21 ++++++++++++++++++
 public/panels/insights.js   | 52 +++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 92 insertions(+), 3 deletions(-)

diff --git a/modules/insights/index.js b/modules/insights/index.js
new file mode 100644
index 0000000..44d41ba
--- /dev/null
+++ b/modules/insights/index.js
@@ -0,0 +1,11 @@
+// Insights (Preview host) — collapses the 8 not-yet-live analytics panels into
+// one tabbed nav entry. No-op mount: it owns no API. insights.js loads the
+// selected panel's html+js into a single content pane and calls that panel's own
+// init(), so those 8 panels stay authoritative and their /api/* routers stay
+// mounted (only their nav entries are hidden via the app.js denylist).
+module.exports = {
+  id: 'insights',
+  title: 'Insights',
+  icon: '📊',
+  mount(_router) { /* intentionally empty — no /api/insights/* routes */ },
+};
diff --git a/modules/registry.js b/modules/registry.js
index a3c357e..2691ebf 100644
--- a/modules/registry.js
+++ b/modules/registry.js
@@ -37,4 +37,5 @@ module.exports = [
   'browse-abandon',     // browse-abandonment drip: Day1 storytelling → Day14 room-setting → Day30 suppress, mock events + dry-run preview (STAGED)
   'playbook',           // monthly playbook: 4-6 suggested campaigns/actions for next 4 weeks from calendar + segment health + perf gaps (Gemini or template)
   'engine',             // Engine: daily AI-suggested social posts with an approve → schedule → post flow (queue via store.js, live via channels.postLive)
+  'insights',           // Insights (Preview host): tabbed host over the 8 not-yet-live analytics panels (performance, segment-perf, send-times, segments, journeys, profiles, follow-counts, ab-tests). No-op mount; those modules stay mounted, only their nav entries are hidden.
 ];
diff --git a/package.json b/package.json
index d557b82..dd0dcf7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "marketing-command-center",
-  "version": "1.9.0",
+  "version": "1.9.1",
   "description": "DW Marketing Command Center — Constant Contact, marketing calendar, suggested copy, on-demand layouts",
   "main": "server.js",
   "scripts": {
diff --git a/public/app.js b/public/app.js
index 4d1e2ab..5e3db1a 100644
--- a/public/app.js
+++ b/public/app.js
@@ -17,14 +17,18 @@ const GROUPS = [
   { name: 'Compose', ids: ['compose', 'linkedin', 'board', 'social'] },
   { name: 'Library', ids: ['assets', 'sounds', 'reels', 'copy', 'layouts', 'templates'] },
   { name: 'Accounts & Email', ids: ['accounts', 'channels', 'clients', 'constant-contact', 'browse-abandon'] },
-  { name: 'Preview · not live yet', ids: ['performance', 'segment-perf', 'send-times', 'segments', 'journeys', 'profiles', 'follow-counts', 'ab-tests'] },
+  { name: 'Insights · preview', ids: ['insights'] },
 ];
 
 // Panels whose ROUTERS stay mounted (their APIs are still called) but whose NAV
 // entries are hidden — because they were merged into a host panel above. Removing
 // them from registry.js would unmount their APIs and break the host, so we hide
 // them here instead. calendars/calendar → calendarhub; engine/composer → compose.
-const HIDDEN = new Set(['calendars', 'calendar', 'engine', 'composer']);
+const HIDDEN = new Set([
+  'calendars', 'calendar', 'engine', 'composer',           // → calendarhub / compose
+  'performance', 'segment-perf', 'send-times', 'segments', // → insights (preview)
+  'journeys', 'profiles', 'follow-counts', 'ab-tests',
+]);
 
 // Always fetch against the bare origin. If the page itself was opened with
 // embedded credentials (http://user:pass@host), relative URLs resolve against
diff --git a/public/panels/insights.html b/public/panels/insights.html
new file mode 100644
index 0000000..5fd8e04
--- /dev/null
+++ b/public/panels/insights.html
@@ -0,0 +1,21 @@
+<!-- Insights — one nav entry over the 8 preview/not-yet-live analytics panels.
+     A wrapping tab bar swaps a single content pane: insights.js loads the picked
+     panel's html+js into #ins-content and runs that panel's own init(). Only one
+     is mounted at a time (no cross-panel id collisions). Backends stay mounted;
+     the 8 panels' nav entries are hidden via the app.js denylist. -->
+<div class="ins">
+  <div class="ins-note">📊 <b>Preview</b> — these views aren't wired to live data yet. Grouped here so they don't clutter the main nav.</div>
+  <div class="ins-tabbar" id="ins-tabs"></div>
+  <div class="ins-content" id="ins-content"><div class="loading">Loading…</div></div>
+</div>
+
+<style>
+  .ins-note{background:var(--cream);border:1px solid var(--line);border-radius:10px;padding:9px 13px;
+    font-size:12.5px;color:var(--ink);margin-bottom:12px}
+  .ins-tabbar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
+  .ins-tab{border:1px solid var(--line);background:#fff;border-radius:9px;cursor:pointer;
+    font:600 12.5px/1 Inter;color:var(--ink);padding:8px 13px}
+  .ins-tab:hover{background:var(--cream)}
+  .ins-tab.active{background:var(--ink);color:#fff;border-color:var(--ink)}
+  .ins-content{min-height:200px}
+</style>
diff --git a/public/panels/insights.js b/public/panels/insights.js
new file mode 100644
index 0000000..50c1929
--- /dev/null
+++ b/public/panels/insights.js
@@ -0,0 +1,52 @@
+// Insights hub — merges the 8 preview analytics panels under one nav entry
+// WITHOUT duplicating their code. A wrapping tab bar swaps a SINGLE content pane:
+// clicking a tab loads that panel's html into #ins-content and calls its own
+// MCC_PANELS[id].init(pane). Single-pane (not 8 panes) so only one panel's DOM
+// exists at a time — no cross-panel id collisions, lighter than mounting all 8.
+window.MCC_PANELS = window.MCC_PANELS || {};
+window.MCC_PANELS['insights'] = {
+  async init(root) {
+    const ORIGIN = location.origin;
+    const TABS = [
+      { id: 'performance',   label: 'Performance' },
+      { id: 'segment-perf',  label: 'Segment Perf' },
+      { id: 'send-times',    label: 'Send Times' },
+      { id: 'segments',      label: 'Segments' },
+      { id: 'journeys',      label: 'Journeys' },
+      { id: 'profiles',      label: 'Profiles' },
+      { id: 'follow-counts', label: 'Follow Counts' },
+      { id: 'ab-tests',      label: 'A/B Tests' },
+    ];
+    const bar = root.querySelector('#ins-tabs');
+    const content = root.querySelector('#ins-content');
+    let current = null;
+
+    async function ensureScript(id) {
+      if (window.MCC_PANELS[id]) return;
+      await new Promise(res => {
+        const s = document.createElement('script');
+        s.src = ORIGIN + '/panels/' + id + '.js';
+        s.onload = res; s.onerror = res;
+        document.body.appendChild(s);
+      });
+    }
+    async function show(id) {
+      if (current === id) return;
+      current = id;
+      bar.querySelectorAll('.ins-tab').forEach(b => b.classList.toggle('active', b.dataset.id === id));
+      content.innerHTML = '<div class="loading">Loading…</div>';
+      try {
+        const r = await fetch(ORIGIN + '/panels/' + id + '.html', { credentials: 'same-origin' });
+        content.innerHTML = r.ok ? await r.text() : `<div class="muted">Could not load ${id}.</div>`;
+      } catch { content.innerHTML = `<div class="muted">Could not load ${id}.</div>`; return; }
+      await ensureScript(id);
+      try {
+        if (window.MCC_PANELS[id] && window.MCC_PANELS[id].init) await window.MCC_PANELS[id].init(content);
+      } catch (e) { console.error('[insights] ' + id + ' init failed', e); }
+    }
+
+    bar.innerHTML = TABS.map(t => `<button type="button" class="ins-tab" data-id="${t.id}">${t.label}</button>`).join('');
+    bar.querySelectorAll('.ins-tab').forEach(btn => btn.onclick = () => show(btn.dataset.id));
+    show(TABS[0].id); // Performance is the default view
+  },
+};

← 39e86ec auto-data-snapshot: 2026-08-12T14:00:30 (1 data files) — pac  ·  back to Marketing Command Center  ·  deploy: Insights hub live on Kamatera (v1.9.1) 454644c →