From db9219319d31d1c6983c9f86d64fe7ba50ec4cca Mon Sep 17 00:00:00 2001 From: you Date: Mon, 23 Mar 2026 00:37:48 +0000 Subject: [PATCH] feat: config-driven customization system (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/config/theme endpoint serving branding, theme colors, node colors, and home page content from config.json with sensible defaults so unconfigured instances look identical to before. Client-side (app.js): - Fetch theme config on page load, before first render - Override CSS variables from theme.* on document root - Override ROLE_COLORS/ROLE_STYLE from nodeColors.* - Replace nav brand text, logo, favicon from branding.* - Store config in window.SITE_CONFIG for other pages Home page (home.js): - Hero title/subtitle from config.home - Steps and checklist from config.home - Footer links from config.home.footerLinks - Chooser welcome text uses configured siteName Config example updated with all available theme options. No default appearance changes β€” all overrides are optional. --- config.example.json | 42 +++++++++++++++++++++++++++++++ public/app.js | 61 +++++++++++++++++++++++++++++++++++++++++++++ public/home.js | 23 +++++++++++------ public/index.html | 4 +-- server.js | 26 +++++++++++++++++++ 5 files changed, 147 insertions(+), 9 deletions(-) diff --git a/config.example.json b/config.example.json index 1a0f55ea..1a6b7df5 100644 --- a/config.example.json +++ b/config.example.json @@ -5,6 +5,48 @@ "cert": "/path/to/cert.pem", "key": "/path/to/key.pem" }, + "branding": { + "siteName": "MeshCore Analyzer", + "tagline": "Real-time MeshCore LoRa mesh network analyzer", + "logoUrl": null, + "faviconUrl": null + }, + "theme": { + "accent": "#4a9eff", + "accentHover": "#6db3ff", + "navBg": "#0f0f23", + "navBg2": "#1a1a2e", + "statusGreen": "#45644c", + "statusYellow": "#b08b2d", + "statusRed": "#b54a4a" + }, + "nodeColors": { + "repeater": "#dc2626", + "companion": "#2563eb", + "room": "#16a34a", + "sensor": "#d97706", + "observer": "#8b5cf6" + }, + "home": { + "heroTitle": "MeshCore Analyzer", + "heroSubtitle": "Find your nodes to start monitoring them.", + "steps": [ + { "emoji": "πŸ“‘", "title": "Connect", "description": "Link your node to the mesh" }, + { "emoji": "πŸ”", "title": "Monitor", "description": "Watch packets flow in real-time" }, + { "emoji": "πŸ“Š", "title": "Analyze", "description": "Understand your network's health" } + ], + "checklist": [ + { "question": "How do I add my node?", "answer": "Search for your node name or paste your public key." }, + { "question": "What regions are covered?", "answer": "Check the map page to see active observers and nodes." } + ], + "footerLinks": [ + { "label": "πŸ“¦ Packets", "url": "#/packets" }, + { "label": "πŸ—ΊοΈ Network Map", "url": "#/map" }, + { "label": "πŸ”΄ Live", "url": "#/live" }, + { "label": "πŸ“‘ All Nodes", "url": "#/nodes" }, + { "label": "πŸ’¬ Channels", "url": "#/channels" } + ] + }, "mqtt": { "broker": "mqtt://localhost:1883", "topic": "meshcore/+/+/packets" diff --git a/public/app.js b/public/app.js index 844cf4a1..90abef06 100644 --- a/public/app.js +++ b/public/app.js @@ -497,6 +497,67 @@ window.addEventListener('DOMContentLoaded', () => { setInterval(updateNavStats, 15000); debouncedOnWS(function () { updateNavStats(); }); + // --- Theme Customization --- + // Fetch theme config and apply branding/colors before first render + fetch('/api/config/theme').then(r => r.json()).then(cfg => { + window.SITE_CONFIG = cfg; + + // Apply CSS variable overrides from theme.* + if (cfg.theme) { + const root = document.documentElement.style; + const varMap = { + accent: '--accent', accentHover: '--accent-hover', + navBg: '--nav-bg', navBg2: '--nav-bg2', + statusGreen: '--status-green', statusYellow: '--status-yellow', statusRed: '--status-red', + text: '--text', textMuted: '--text-muted', border: '--border', + surface0: '--surface-0', surface1: '--surface-1', surface2: '--surface-2', surface3: '--surface-3', + cardBg: '--card-bg', contentBg: '--content-bg', inputBg: '--input-bg', + rowStripe: '--row-stripe', rowHover: '--row-hover', detailBg: '--detail-bg', + selectedBg: '--selected-bg' + }; + for (const [key, cssVar] of Object.entries(varMap)) { + if (cfg.theme[key]) root.setProperty(cssVar, cfg.theme[key]); + } + // Also update nav gradient if navBg is customized + if (cfg.theme.navBg) { + const nav = document.querySelector('.top-nav'); + if (nav) nav.style.background = `linear-gradient(135deg, ${cfg.theme.navBg} 0%, ${cfg.theme.navBg2 || cfg.theme.navBg} 50%, ${cfg.theme.navBg} 100%)`; + } + } + + // Apply node color overrides to ROLE_COLORS and ROLE_STYLE + if (cfg.nodeColors) { + for (const [role, color] of Object.entries(cfg.nodeColors)) { + if (window.ROLE_COLORS && role in window.ROLE_COLORS) window.ROLE_COLORS[role] = color; + if (window.ROLE_STYLE && window.ROLE_STYLE[role]) window.ROLE_STYLE[role].color = color; + } + } + + // Apply branding + if (cfg.branding) { + if (cfg.branding.siteName) { + document.title = cfg.branding.siteName; + const brandText = document.querySelector('.brand-text'); + if (brandText) brandText.textContent = cfg.branding.siteName; + } + if (cfg.branding.logoUrl) { + const brandIcon = document.querySelector('.brand-icon'); + if (brandIcon) { + const img = document.createElement('img'); + img.src = cfg.branding.logoUrl; + img.alt = cfg.branding.siteName || 'Logo'; + img.style.height = '24px'; + img.style.width = 'auto'; + brandIcon.replaceWith(img); + } + } + if (cfg.branding.faviconUrl) { + const favicon = document.querySelector('link[rel="icon"]'); + if (favicon) favicon.href = cfg.branding.faviconUrl; + } + } + }).catch(() => { window.SITE_CONFIG = null; }); + if (!location.hash || location.hash === '#/') location.hash = '#/home'; else navigate(); }); diff --git a/public/home.js b/public/home.js index b07fb35b..53f0fd2e 100644 --- a/public/home.js +++ b/public/home.js @@ -39,7 +39,7 @@ function showChooser(container) { container.innerHTML = `
-

Welcome to Bay Area MeshCore Analyzer

+

Welcome to ${escapeHtml(window.SITE_CONFIG?.branding?.siteName || 'MeshCore Analyzer')}

How familiar are you with MeshCore?