[object Object]

← back to Commercialrealestate

CRCP: top-right signed-in user badge (person icon + name + sign out)

9b4f89b463912fb843181de613f8f85ec319edd3 · 2026-08-20 11:10:18 -0700 · Steve

Adds public/user-badge.js — a self-contained top-right pill (person icon +
signed-in name, click for details + Sign out) injected into every served HTML
page by a serve.js middleware, so it needs zero edits to any page file (the
TK-10703 session owns those). Hides any legacy inline #auth so there's one
consistent badge. Skips login.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 9b4f89b463912fb843181de613f8f85ec319edd3
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 20 11:10:18 2026 -0700

    CRCP: top-right signed-in user badge (person icon + name + sign out)
    
    Adds public/user-badge.js — a self-contained top-right pill (person icon +
    signed-in name, click for details + Sign out) injected into every served HTML
    page by a serve.js middleware, so it needs zero edits to any page file (the
    TK-10703 session owns those). Hides any legacy inline #auth so there's one
    consistent badge. Skips login.html.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/user-badge.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/serve.js     |  22 +++++++++++
 2 files changed, 125 insertions(+)

diff --git a/public/user-badge.js b/public/user-badge.js
new file mode 100644
index 0000000..8366c84
--- /dev/null
+++ b/public/user-badge.js
@@ -0,0 +1,103 @@
+/* user-badge.js — top-right "who's signed in" chrome for every CRCP page (Steve 2026-08-20).
+ * Self-contained, zero-dependency. Injected into every served HTML page by serve.js, so it needs
+ * no edits to any page file. Fetches /api/me; if a real user is signed in it renders a fixed
+ * top-right person-icon pill (name + caret) that opens a small menu with the user's details and a
+ * Sign out action. Hides any page's legacy inline #auth indicator so there's one consistent badge.
+ */
+(function () {
+  'use strict';
+  if (window.__crcpUserBadge) return; window.__crcpUserBadge = true;
+  // Never on the sign-in page itself.
+  if (/\/login\.html$/.test(location.pathname)) return;
+
+  var prettyRole = function (r) {
+    if (!r) return '';
+    return String(r).replace(/[_-]+/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
+  };
+  var esc = function (s) {
+    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
+      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
+    });
+  };
+
+  var PERSON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" ' +
+    'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
+    '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>';
+  var CARET = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" ' +
+    'stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
+    '<polyline points="6 9 12 15 18 9"></polyline></svg>';
+
+  function injectStyle() {
+    if (document.getElementById('crcp-ub-style')) return;
+    var css = '' +
+      '#crcp-ub{position:fixed;top:8px;right:12px;z-index:99999;font:13px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}' +
+      '#crcp-ub .pill{display:flex;align-items:center;gap:7px;padding:6px 10px;border-radius:999px;cursor:pointer;' +
+      'background:rgba(19,24,34,.92);color:#eef2f8;border:1px solid rgba(217,178,95,.45);backdrop-filter:blur(6px);' +
+      '-webkit-backdrop-filter:blur(6px);box-shadow:0 4px 14px rgba(0,0,0,.35);user-select:none;white-space:nowrap}' +
+      '#crcp-ub .pill:hover{border-color:#d9b25f}' +
+      '#crcp-ub .ico{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;' +
+      'background:linear-gradient(135deg,#d9b25f,#a67c2e);color:#1a130a;flex:0 0 auto}' +
+      '#crcp-ub .nm{font-weight:600;max-width:180px;overflow:hidden;text-overflow:ellipsis}' +
+      '#crcp-ub .cr{opacity:.7;display:flex}' +
+      '#crcp-ub .menu{position:absolute;top:calc(100% + 6px);right:0;min-width:200px;background:#131822;' +
+      'border:1px solid #232c3b;border-radius:12px;box-shadow:0 18px 44px rgba(0,0,0,.5);padding:12px 14px;display:none}' +
+      '#crcp-ub.open .menu{display:block}' +
+      '#crcp-ub .mline{color:#8b97a8;font-size:12px;margin:2px 0}' +
+      '#crcp-ub .mname{color:#eef2f8;font-size:14px;font-weight:600;margin-bottom:2px}' +
+      '#crcp-ub .mco{color:#d9b25f}' +
+      '#crcp-ub .divider{height:1px;background:#232c3b;margin:10px -14px}' +
+      '#crcp-ub .out{display:block;width:100%;text-align:left;background:none;border:0;color:#3b7dff;' +
+      'font-size:13px;cursor:pointer;padding:2px 0}' +
+      '#crcp-ub .out:hover{text-decoration:underline}';
+    var s = document.createElement('style'); s.id = 'crcp-ub-style'; s.textContent = css;
+    document.head.appendChild(s);
+  }
+
+  function render(me) {
+    // Hide any legacy inline sign-in indicator so we don't show it twice.
+    var legacy = document.getElementById('auth'); if (legacy) legacy.style.display = 'none';
+
+    var name = me.name || me.username || me.email;
+    var role = prettyRole(me.role);
+    var wrap = document.createElement('div'); wrap.id = 'crcp-ub';
+    wrap.innerHTML =
+      '<div class="pill" role="button" tabindex="0" aria-haspopup="true" aria-expanded="false" ' +
+      'title="Signed in as ' + esc(name) + '">' +
+        '<span class="ico">' + PERSON + '</span>' +
+        '<span class="nm">' + esc(name) + '</span>' +
+        '<span class="cr">' + CARET + '</span>' +
+      '</div>' +
+      '<div class="menu" role="menu">' +
+        '<div class="mname">' + esc(name) + '</div>' +
+        (me.company ? '<div class="mline mco">' + esc(me.company) + '</div>' : '') +
+        (role ? '<div class="mline">' + esc(role) + '</div>' : '') +
+        '<div class="mline">' + esc(me.email || me.username) + '</div>' +
+        '<div class="divider"></div>' +
+        '<button class="out" type="button">Sign out</button>' +
+      '</div>';
+    document.body.appendChild(wrap);
+
+    var pill = wrap.querySelector('.pill');
+    var toggle = function () {
+      var open = wrap.classList.toggle('open');
+      pill.setAttribute('aria-expanded', open ? 'true' : 'false');
+    };
+    pill.addEventListener('click', function (e) { e.stopPropagation(); toggle(); });
+    pill.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
+    document.addEventListener('click', function () { wrap.classList.remove('open'); pill.setAttribute('aria-expanded', 'false'); });
+    wrap.querySelector('.menu').addEventListener('click', function (e) { e.stopPropagation(); });
+    wrap.querySelector('.out').addEventListener('click', async function () {
+      try { await fetch('/auth/logout', { method: 'POST' }); } catch (_) {}
+      location.href = '/login.html';
+    });
+  }
+
+  function boot() {
+    fetch('/api/me', { headers: { Accept: 'application/json' } })
+      .then(function (r) { return r.ok ? r.json() : null; })
+      .then(function (me) { if (me && (me.email || me.username)) { injectStyle(); render(me); } })
+      .catch(function () {});
+  }
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
+  else boot();
+})();
diff --git a/scripts/serve.js b/scripts/serve.js
index bd88898..3a3e470 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -59,6 +59,28 @@ app.use((req, res, next) => {
   return res.status(401).json({ error: 'sign in' });
 });
 
+// ── Signed-in user badge injection (Steve 2026-08-20) ────────────────────────────────
+// Inject ONE shared <script> into every served HTML page so a person-icon + "who's signed in"
+// (+ Sign out) renders top-right on EVERY page — with NO edits to any page file (the TK-10703
+// session owns those). Runs before the page routes + static mount; only touches .html navigations
+// (and '/'), reads the file from /public, and drops the tag before </body>. login.html is skipped
+// so the sign-in page stays pristine. Anything not a readable public html file falls through.
+const PUB_DIR = path.join(ROOT, 'public');
+const BADGE_TAG = '<script src="/user-badge.js" defer></script>';
+app.get(/(?:^\/$|\.html$)/, (req, res, next) => {
+  const rel = req.path === '/' ? 'index.html' : req.path.replace(/^\/+/, '');
+  if (rel === 'login.html') return next();                 // sign-in page stays badge-free
+  const file = path.join(PUB_DIR, rel);
+  if (!file.startsWith(PUB_DIR + path.sep)) return next();  // path-traversal guard
+  fs.readFile(file, 'utf8', (err, html) => {
+    if (err) return next();                                 // not a public html file -> other routes
+    const out = html.includes('/user-badge.js') ? html
+      : html.includes('</body>') ? html.replace(/<\/body>/i, BADGE_TAG + '</body>')
+      : html + BADGE_TAG;
+    res.type('html').send(out);
+  });
+});
+
 // ── P1 subscription layer: accounts + saved searches + watchlist (docs/TOOL-SPEC.md) ──
 try { acct(app, ROOT); require('./crcp-billing')(app, ROOT, acct.userOf); require('./crcp-leads')(app, ROOT, acct.userOf); require('./crcp-export')(app, ROOT, acct.userOf); require('./crcp-notes')(app, ROOT, acct.userOf); } catch (e) { console.error('[crcp-accounts/billing/leads/export/notes] mount failed:', e.message); }
 

← 05eae71 CRCP: replace whole-site Basic-auth popup with single real-u  ·  back to Commercialrealestate  ·  CRCP index: bake full nav-agent skillset into UI — list-on-l be1fb30 →