feat(nodes): add CSV/HTML export of the node list (#3499)#3537
Conversation
Adds an "Export: CSV / HTML" control to the node-list sidebar that exports the currently-displayed nodes (respecting active filters and sort order) entirely client-side. Columns: Long Name, Short Name, Node ID, Hardware, Role, Firmware, Hops Away, SNR (dB), RSSI (dBm), Battery (%), Voltage (V), Channel, Latitude, Longitude, Last Heard. Signal/hops use current values with honest labels (MeshMonitor stores instantaneous node state, not historical averages). - src/utils/nodeExport.ts: pure, tested CSV (RFC 4180) + HTML builders and an isolated downloadTextFile helper - NodesTab: shared displayedNodes memo (render + export can't drift), export handler and buttons - nodes.css + en.json: styling and i18n keys Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj
|
Claude finished @Yeraze's task —— View job PR Review: feat(nodes): add CSV/HTML export of the node list
Overall this is a clean, well-structured implementation. The separation of pure transform logic ( Bugs1. Memory leak in export function downloadTextFile(filename: string, content: string, mimeType: string): void {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url); // ← only reached if nothing above throws
}If any call between const url = URL.createObjectURL(blob);
try {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} finally {
URL.revokeObjectURL(url);
}2. RFC 4180 CSV — missing final CRLF ( RFC 4180 §2.4 says "Each record is located on a separate line, delimited by a line break (CRLF)" and §2.5 "The last record in the file may or may not have an ending line break." Most consumers handle the absent terminator fine, but some strict parsers don't. Consider appending 3. it('emits a header row matching the column labels', () => {
const csv = nodesToCsv([]);
expect(csv).toBe(NODE_EXPORT_COLUMNS.map((c) => c.label).join(','));This test would silently break if a label ever gained a comma or quote (e.g. Performance4. Export menu re-register on every useEffect(() => {
if (!showExportMenu) return;
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('keydown', onKey);
return () => { ... };
}, [showExportMenu]);This is the standard pattern for "attach only while open" and is fine as-is. No action needed; called out only because it's a common area of confusion. 5. const sortedNodes = displayedNodes;This is purely a cosmetic rename inside the render block. It's harmless but adds a little noise; could just use Security6. HTML export: const subtitle = [
`${rows.length} node${rows.length === 1 ? '' : 's'}`,
opts.generatedAt ? `generated ${opts.generatedAt}` : null,
]
.filter(Boolean)
.join(' • ');
7. function escapeHtml(value: string): string {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}Single quotes ( Minor code quality / UX8. Export button uses ⬇ (U+2B07 DOWNWARDS BLACK ARROW) ( ⬇ is semantically a "down arrow" but the tooltip says "Export node list." The file-download idiom in the rest of the UI uses different icons (e.g. audit log uses a CSV label). Minor UX nit — no action needed if the team is happy with it. 9.
Test coverageThe test file is thorough for the pure transform functions. One gap: 10. No test for 11. Summary
The memory leak in |
Summary
Adds a way to export the node list as CSV or HTML for mesh-upgrade planning (requested in #3499). The Nodes tab sidebar gets a compact ⬇ export button in the existing filter/sort row with a CSV / HTML dropdown — no extra vertical space. Export runs entirely client-side from data already loaded in the browser, so there's no new backend route or query.
Changes
src/utils/nodeExport.ts(new): pure, framework-agnostic transforms —buildNodeExportRows,nodesToCsv(RFC 4180, CRLF),nodesToHtml(standalone styled/printable doc, HTML-escaped against injection), plus an isolateddownloadTextFileDOM helper.src/components/NodesTab.tsx: extracted the list's filter + favorites-first sort into a shareddisplayedNodesmemo so the rendered list and the export can't drift; added the export handler and the icon + dropdown menu (closes on outside-click / Esc; disabled when the list is empty).src/styles/nodes.css: styling for the export icon button and dropdown menu.public/locales/en.json: 3 newnodes.export*i18n keys (other locales fall back viat()defaults).CHANGELOG.md: Unreleased → Features entry.Behavior
Issues Resolved
Fixes #3499
Documentation Updates
Testing
src/utils/nodeExport.test.ts(11 tests): field mapping, blank handling, local-node-as-0-hops, unknown-hops blanking, CSV escaping (commas/quotes/newlines), HTML injection escaping, subtitle/count🤖 Generated with Claude Code