mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 07:55:04 +00:00
feat: include favorites and claimed nodes in export/import JSON (#1003)
## Summary
Extends the customizer v2 export/import to include favorite nodes and
claimed ("My Mesh") nodes, so users can transfer their full setup
between browsers/devices.
## Changes
### `public/customize-v2.js`
- `readOverrides()` now merges `favorites` (from `meshcore-favorites`)
and `myNodes` (from `meshcore-my-nodes`) into the exported JSON
- `writeOverrides()` extracts `favorites`/`myNodes` arrays and writes
them to their respective localStorage keys, keeping theme overrides
separate
- `validateShape()` validates both new keys as arrays, rejecting
non-array values
- `VALID_SECTIONS` updated to include `favorites` and `myNodes`
### `test-customizer-v2.js`
- 8 new tests covering read/write/validate for both favorites and
myNodes
## TDD
- Red commit: `0405fb7` (failing tests)
- Green commit: `bb9dc34` (implementation)
Fixes #895
---------
Co-authored-by: you <you@example.com>
This commit is contained in:
+39
-6
@@ -33,7 +33,7 @@
|
||||
'meshcore-live-heatmap-opacity'
|
||||
];
|
||||
|
||||
var VALID_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps', 'heatmapOpacity', 'liveHeatmapOpacity', 'distanceUnit'];
|
||||
var VALID_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps', 'heatmapOpacity', 'liveHeatmapOpacity', 'distanceUnit', 'favorites', 'myNodes'];
|
||||
var OBJECT_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps'];
|
||||
var SCALAR_SECTIONS = ['heatmapOpacity', 'liveHeatmapOpacity'];
|
||||
var DISTANCE_UNIT_VALUES = ['km', 'mi', 'auto'];
|
||||
@@ -313,9 +313,17 @@
|
||||
function readOverrides() {
|
||||
try {
|
||||
var raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw == null) return {};
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
||||
var parsed = (raw != null) ? JSON.parse(raw) : {};
|
||||
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) parsed = {};
|
||||
// Include favorites and claimed nodes from their own localStorage keys
|
||||
try {
|
||||
var favs = JSON.parse(localStorage.getItem('meshcore-favorites') || '[]');
|
||||
if (Array.isArray(favs) && favs.length) parsed.favorites = favs;
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
var myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]');
|
||||
if (Array.isArray(myNodes) && myNodes.length) parsed.myNodes = myNodes;
|
||||
} catch (e) { /* ignore */ }
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
return {};
|
||||
@@ -386,14 +394,28 @@
|
||||
|
||||
function writeOverrides(delta) {
|
||||
if (delta == null || typeof delta !== 'object') return;
|
||||
// Extract favorites/myNodes and store in their own localStorage keys
|
||||
if (Array.isArray(delta.favorites)) {
|
||||
try { localStorage.setItem('meshcore-favorites', JSON.stringify(delta.favorites)); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (Array.isArray(delta.myNodes)) {
|
||||
try { localStorage.setItem('meshcore-my-nodes', JSON.stringify(delta.myNodes)); } catch (e) { /* ignore */ }
|
||||
}
|
||||
// Build theme-only delta (without favorites/myNodes)
|
||||
var themeDelta = {};
|
||||
for (var k in delta) {
|
||||
if (delta.hasOwnProperty(k) && k !== 'favorites' && k !== 'myNodes') {
|
||||
themeDelta[k] = delta[k];
|
||||
}
|
||||
}
|
||||
// If empty, remove key entirely
|
||||
var keys = Object.keys(delta);
|
||||
var keys = Object.keys(themeDelta);
|
||||
if (keys.length === 0) {
|
||||
try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ }
|
||||
_updateSaveStatus('saved');
|
||||
return;
|
||||
}
|
||||
var validated = _validateDelta(delta);
|
||||
var validated = _validateDelta(themeDelta);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(validated));
|
||||
_updateSaveStatus('saved');
|
||||
@@ -758,6 +780,17 @@
|
||||
if (key === 'distanceUnit' && DISTANCE_UNIT_VALUES.indexOf(obj[key]) === -1) {
|
||||
errors.push('Invalid distanceUnit: "' + obj[key] + '" — must be km, mi, or auto');
|
||||
}
|
||||
// Validate favorites and myNodes arrays
|
||||
if (key === 'favorites') {
|
||||
if (!Array.isArray(obj[key])) {
|
||||
errors.push('"favorites" must be an array of public key strings');
|
||||
}
|
||||
}
|
||||
if (key === 'myNodes') {
|
||||
if (!Array.isArray(obj[key])) {
|
||||
errors.push('"myNodes" must be an array of node objects');
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: errors.length === 0, errors: errors };
|
||||
}
|
||||
|
||||
@@ -512,6 +512,69 @@ test('existing user overrides are NOT pruned by setOverride on other keys', () =
|
||||
assert.strictEqual(delta.theme.border, '#00ff00', 'new non-matching override should be stored');
|
||||
});
|
||||
|
||||
// ── Fix #895: export/import includes favorites and claimed nodes ──
|
||||
|
||||
test('readOverrides includes favorites from localStorage', () => {
|
||||
const { api, ls } = loadCustomizer();
|
||||
api.init({});
|
||||
ls.setItem('meshcore-favorites', JSON.stringify(['abc123', 'def456']));
|
||||
const data = api.readOverrides();
|
||||
assert.deepStrictEqual(data.favorites, ['abc123', 'def456'], 'favorites should be included in export');
|
||||
});
|
||||
|
||||
test('readOverrides includes myNodes from localStorage', () => {
|
||||
const { api, ls } = loadCustomizer();
|
||||
api.init({});
|
||||
ls.setItem('meshcore-my-nodes', JSON.stringify([{pubkey: 'abc123', name: 'Node1', addedAt: 1000}]));
|
||||
const data = api.readOverrides();
|
||||
assert.deepStrictEqual(data.myNodes, [{pubkey: 'abc123', name: 'Node1', addedAt: 1000}], 'myNodes should be included in export');
|
||||
});
|
||||
|
||||
test('writeOverrides restores favorites to localStorage', () => {
|
||||
const { api, ls } = loadCustomizer();
|
||||
api.init({});
|
||||
api.writeOverrides({ favorites: ['abc123', 'def456'] });
|
||||
const favs = JSON.parse(ls.getItem('meshcore-favorites') || '[]');
|
||||
assert.deepStrictEqual(favs, ['abc123', 'def456'], 'favorites should be written to meshcore-favorites');
|
||||
});
|
||||
|
||||
test('writeOverrides restores myNodes to localStorage', () => {
|
||||
const { api, ls } = loadCustomizer();
|
||||
api.init({});
|
||||
const nodes = [{pubkey: 'abc123', name: 'Node1', addedAt: 1000}];
|
||||
api.writeOverrides({ myNodes: nodes });
|
||||
const stored = JSON.parse(ls.getItem('meshcore-my-nodes') || '[]');
|
||||
assert.deepStrictEqual(stored, nodes, 'myNodes should be written to meshcore-my-nodes');
|
||||
});
|
||||
|
||||
test('validateShape accepts favorites array', () => {
|
||||
const { api } = loadCustomizer();
|
||||
api.init({});
|
||||
const result = api.validateShape({ favorites: ['abc123'] });
|
||||
assert.ok(result.valid, 'favorites array should be valid');
|
||||
});
|
||||
|
||||
test('validateShape accepts myNodes array', () => {
|
||||
const { api } = loadCustomizer();
|
||||
api.init({});
|
||||
const result = api.validateShape({ myNodes: [{pubkey: 'abc', name: 'N', addedAt: 1}] });
|
||||
assert.ok(result.valid, 'myNodes array should be valid');
|
||||
});
|
||||
|
||||
test('validateShape rejects non-array favorites', () => {
|
||||
const { api } = loadCustomizer();
|
||||
api.init({});
|
||||
const result = api.validateShape({ favorites: 'not-an-array' });
|
||||
assert.ok(!result.valid, 'non-array favorites should be invalid');
|
||||
});
|
||||
|
||||
test('validateShape rejects non-array myNodes', () => {
|
||||
const { api } = loadCustomizer();
|
||||
api.init({});
|
||||
const result = api.validateShape({ myNodes: 'not-an-array' });
|
||||
assert.ok(!result.valid, 'non-array myNodes should be invalid');
|
||||
});
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user