mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 03:44:13 +00:00
feat: server-side theme save/load via theme.json
- Server reads from theme.json (separate from config.json), hot-loads on every request - POST /api/config/theme writes theme.json directly — no manual file editing - GET /api/config/theme now merges: defaults → config.json → theme.json - Also returns themeDark and typeColors (were missing from API) - Customizer: replaced 'merge into config.json' instructions with Save/Load Server buttons - JSON export moved to collapsible details section - theme.json added to .gitignore (instance-specific)
This commit is contained in:
@@ -6,3 +6,4 @@ data/
|
||||
config.json
|
||||
data-lincomatic/
|
||||
config-lincomatic.json
|
||||
theme.json
|
||||
|
||||
+48
-10
@@ -597,18 +597,19 @@
|
||||
(hasUserTheme ? '<button class="cust-reset-user" id="custResetUser">🗑️ Reset my theme</button>' : '') +
|
||||
'</div>' +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Admin Export</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Export as config.json for server deployment — applies to all users of this instance.</p>' +
|
||||
'<textarea class="cust-export-area" readonly id="custExportJson">' + esc(json) + '</textarea>' +
|
||||
'<p class="cust-section-title">Admin — Server Theme</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Save/load the theme on the server. Applies to all users who haven\'t set their own.</p>' +
|
||||
'<div class="cust-export-btns" style="margin-bottom:12px">' +
|
||||
'<button class="cust-save-user" id="custSaveServer">📡 Save to Server</button>' +
|
||||
'<button class="cust-dl-btn" id="custLoadServer">📥 Load from Server</button>' +
|
||||
'</div>' +
|
||||
'<details style="margin-top:8px"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted)">Export as JSON</summary>' +
|
||||
'<textarea class="cust-export-area" readonly id="custExportJson" style="margin-top:8px">' + esc(json) + '</textarea>' +
|
||||
'<div class="cust-export-btns">' +
|
||||
'<button class="cust-copy-btn" id="custCopy">📋 Copy to Clipboard</button>' +
|
||||
'<button class="cust-dl-btn" id="custDownload">💾 Download config-theme.json</button>' +
|
||||
'</div>' +
|
||||
'<div class="cust-instructions">' +
|
||||
'<strong>How to apply:</strong><br>' +
|
||||
'Merge this JSON into your <code>config.json</code> file and restart the server.<br>' +
|
||||
'Only values that differ from defaults are included.' +
|
||||
'<button class="cust-copy-btn" id="custCopy">📋 Copy</button>' +
|
||||
'<button class="cust-dl-btn" id="custDownload">💾 Download</button>' +
|
||||
'</div>' +
|
||||
'</details>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -864,6 +865,43 @@
|
||||
render(container);
|
||||
applyThemePreview(); autoSave();
|
||||
});
|
||||
|
||||
// Save to server
|
||||
var saveServerBtn = document.getElementById('custSaveServer');
|
||||
if (saveServerBtn) saveServerBtn.addEventListener('click', function () {
|
||||
var data = buildExport();
|
||||
fetch('/api/config/theme', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('Server returned ' + r.status);
|
||||
return r.json();
|
||||
}).then(function () {
|
||||
saveServerBtn.textContent = '✓ Saved to server!';
|
||||
setTimeout(function () { saveServerBtn.textContent = '📡 Save to Server'; }, 2000);
|
||||
}).catch(function (e) {
|
||||
saveServerBtn.textContent = '✕ Failed: ' + e.message;
|
||||
setTimeout(function () { saveServerBtn.textContent = '📡 Save to Server'; }, 3000);
|
||||
});
|
||||
});
|
||||
|
||||
// Load from server
|
||||
var loadServerBtn = document.getElementById('custLoadServer');
|
||||
if (loadServerBtn) loadServerBtn.addEventListener('click', function () {
|
||||
fetch('/api/config/theme').then(function (r) { return r.json(); }).then(function (cfg) {
|
||||
window.SITE_CONFIG = cfg;
|
||||
resetPreview();
|
||||
initState();
|
||||
applyThemePreview();
|
||||
render(container);
|
||||
loadServerBtn.textContent = '✓ Loaded!';
|
||||
setTimeout(function () { loadServerBtn.textContent = '📥 Load from Server'; }, 2000);
|
||||
}).catch(function (e) {
|
||||
loadServerBtn.textContent = '✕ Failed';
|
||||
setTimeout(function () { loadServerBtn.textContent = '📥 Load from Server'; }, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
||||
@@ -383,19 +383,32 @@ function getObserverIdsForRegions(regionParam) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Theme: load from separate theme.json (falls back to config.json keys for compat)
|
||||
const THEME_PATH = path.join(__dirname, 'theme.json');
|
||||
function loadThemeFile() {
|
||||
try { return JSON.parse(fs.readFileSync(THEME_PATH, 'utf8')); } catch { return {}; }
|
||||
}
|
||||
|
||||
app.get('/api/config/theme', (req, res) => {
|
||||
const theme = loadThemeFile();
|
||||
res.json({
|
||||
branding: {
|
||||
siteName: 'MeshCore Analyzer',
|
||||
tagline: 'Real-time MeshCore LoRa mesh network analyzer',
|
||||
...(config.branding || {})
|
||||
...(config.branding || {}),
|
||||
...(theme.branding || {})
|
||||
},
|
||||
theme: {
|
||||
accent: '#4a9eff',
|
||||
accentHover: '#6db3ff',
|
||||
navBg: '#0f0f23',
|
||||
navBg2: '#1a1a2e',
|
||||
...(config.theme || {})
|
||||
...(config.theme || {}),
|
||||
...(theme.theme || {})
|
||||
},
|
||||
themeDark: {
|
||||
...(config.themeDark || {}),
|
||||
...(theme.themeDark || {})
|
||||
},
|
||||
nodeColors: {
|
||||
repeater: '#dc2626',
|
||||
@@ -403,12 +416,28 @@ app.get('/api/config/theme', (req, res) => {
|
||||
room: '#16a34a',
|
||||
sensor: '#d97706',
|
||||
observer: '#8b5cf6',
|
||||
...(config.nodeColors || {})
|
||||
...(config.nodeColors || {}),
|
||||
...(theme.nodeColors || {})
|
||||
},
|
||||
home: config.home || null,
|
||||
typeColors: {
|
||||
...(config.typeColors || {}),
|
||||
...(theme.typeColors || {})
|
||||
},
|
||||
home: theme.home || config.home || null,
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/config/theme', express.json(), (req, res) => {
|
||||
try {
|
||||
const data = req.body;
|
||||
if (!data || typeof data !== 'object') return res.status(400).json({ error: 'Invalid JSON' });
|
||||
fs.writeFileSync(THEME_PATH, JSON.stringify(data, null, 2), 'utf8');
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/config/map', (req, res) => {
|
||||
const defaults = config.mapDefaults || {};
|
||||
res.json({
|
||||
|
||||
Reference in New Issue
Block a user