## Problem
Fixes#325. Removing all home steps and clicking "Reset my theme" did
not restore them.
## Root cause
Two-part bug:
**1. `SITE_CONFIG.home` permanently mutated at page load**
`app.js` calls `mergeUserHomeConfig(SITE_CONFIG, userTheme)` which does
`SITE_CONFIG.home = Object.assign({}, serverHome, userTheme.home)`. If
the user had `steps: []` saved in localStorage, this sets
`SITE_CONFIG.home.steps = []` globally — permanently for the lifetime of
the page.
**2. `initState()` reads the contaminated config**
When the customizer opens (or Reset is clicked), `initState()` reads
`cfg = window.SITE_CONFIG`. Since `SITE_CONFIG.home.steps` is already
`[]`, `state.home.steps` stays `[]` even after
`localStorage.removeItem`. `autoSave()` then re-saves `steps: []`
straight back.
**Secondary issue:** `data-rm-step` / add / move handlers didn't call
`autoSave()`, making step persistence non-deterministic (only saved if a
text field edit happened to be pending).
## Fix
- **`app.js`**: snapshot `SITE_CONFIG.home` before `mergeUserHomeConfig`
→ `window._SITE_CONFIG_ORIGINAL_HOME`
- **`customize.js`**: `initState()` uses `_SITE_CONFIG_ORIGINAL_HOME`
instead of the contaminated `cfg.home`
- **`customize.js`**: add `autoSave()` to rm/move/add handlers for
steps, checklist, and footer links
## Tests
2 new unit tests covering the snapshot bypass and DEFAULTS fallback. 231
tests pass.
## Checklist
- [x] Branches from `upstream/master`
- [x] No Matomo or local-only commits
- [x] Cache busters bumped
- [x] 231 tests pass, 0 fail
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Implements issue #286 P1 frontend timestamp features on top of P0:
- Added global timestamp timezone toggle in Display tab (`Local time` /
`UTC`)
- Added absolute-mode timestamp format presets (`iso`, `iso-seconds`,
`locale`)
- Added optional custom format input (only when
`SITE_CONFIG.timestamps.allowCustomFormat === true`)
- Extended `formatTimestamp()` / `formatTimestampWithTooltip()` behavior
to honor timezone + format settings
- Preserved server defaults with localStorage override precedence
- Bumped `public/index.html` cache busters in same commit
## Details
### 1) Timezone toggle
- New Display tab control persisted to `meshcore-timestamp-timezone`
- Reads server default from `window.SITE_CONFIG.timestamps.timezone`
with fallback to `local`
- Formatting logic now supports both local and UTC absolute rendering
### 2) Format presets (absolute mode only)
- New Display tab preset dropdown (shown only when timestamp mode =
`absolute`)
- Presets implemented:
- `iso` → `YYYY-MM-DD HH:mm:ss`
- `iso-seconds` → `YYYY-MM-DD HH:mm:ss.SSS`
- `locale` → `toLocaleString()` (or UTC locale when timezone=utc)
- Persisted to `meshcore-timestamp-format`
- Reads server default from `window.SITE_CONFIG.timestamps.formatPreset`
(fallback `iso`)
### 3) Custom format string (guarded)
- Text input only renders when
`window.SITE_CONFIG.timestamps.allowCustomFormat` is `true`
- Persisted to `meshcore-timestamp-custom-format`
- If non-empty and enabled, custom format overrides preset
- Frontend intentionally does not hard-validate the format string;
unsupported patterns fall back to preset behavior
## Tests
Executed required test commands:
```bash
node test-frontend-helpers.js
node test-packet-filter.js
node test-aging.js
```
Added coverage in `test-frontend-helpers.js` for:
- UTC output behavior
- Local output behavior
- `iso-seconds` includes milliseconds
- `locale` format behavior
All passed locally.
Refs #286
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
## Fix: FAQ save no longer wipes other home page sections
Fixes#284
### Problem
Editing FAQ in the customizer and saving caused other home page sections
(steps, footer links, hero text) to disappear on reload. Colors could
also reset.
### Root cause
`initState()` in `customize.js` used `||` (OR) logic for the `home`
object — if localStorage had *any* `home.checklist`, it took that and
ignored the server config for other fields. Partial localStorage data
replaced the full server config instead of merging on top.
### Fix
Changed `initState()` to properly layer: `DEFAULTS → server config →
localStorage` for all sections. Each field merges independently — a
partial localStorage save (e.g., only checklist) no longer wipes steps,
footerLinks, or hero fields. Same merge pattern applied to all theme
sections for consistency.
### Files changed
- `public/customize.js` — `initState()` merge logic
- `public/index.html` — cache buster bump
- `test-frontend-helpers.js` — regression tests:
1. Partial localStorage (checklist only) preserves steps/footerLinks
2. Server config values survive partial local overrides
3. Full localStorage properly overrides server config
### Testing
- `node test-frontend-helpers.js` ✅
- `node test-packet-filter.js` ✅
- `node test-aging.js` ✅
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
- Add a new **Display** tab to the customizer tab bar (between Home Page
and Export / Save).
- Move timestamp-related **UI Settings** out of Branding into the new
Display tab.
- Keep Branding focused on site identity fields (name, tagline, logo,
favicon).
- Bump `public/index.html` cache busters so updated frontend assets load
immediately.
## Testing
- `node test-frontend-helpers.js`
- `node test-packet-filter.js`
- `node test-aging.js`
Fixes#293
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#210: Add role="img" aria-label to 9 Chart.js canvases in node-analytics.js
and observer-detail.js with descriptive labels.
#211: Add scope="col" to all <th> elements across analytics.js, audio-lab.js,
compare.js, node-analytics.js, nodes.js, observer-detail.js, observers.js,
and packets.js (40+ headers).
#212: Add aria-label to packet filter input and time window select in
packets.js. Add for/id associations to all customize.js inputs: branding,
theme colors, node/type colors, heatmap sliders, onboarding fields, and
export controls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Issues fixed:
- #127: Firefox copy URL - shared copyToClipboard() with execCommand fallback
- #125: Dismiss packet detail pane - close button with keyboard support
- #124: Customize window scrollbar - flex layout fix for overflow
- #122: Last Activity stale times - use last_heard || last_seen
Test improvements:
- E2E perf: replace 19 networkidle waits, cut navigations 14->7, remove 11 sleeps
- 8 new unit tests for copyToClipboard helper (47->55 in test-frontend-helpers)
- 1 new E2E test for packet pane dismiss
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Live page showHeatMap() now reads meshcore-live-heatmap-opacity from
localStorage and applies it to the canvas element (was hardcoded 0.3)
- Customizer now has two clearly labeled sliders:
🗺️ Nodes Map — controls the static map page heatmap
📡 Live Map — controls the live page heatmap
- Each uses its own localStorage key (meshcore-heatmap-opacity vs
meshcore-live-heatmap-opacity)
- Added E2E tests for live opacity persistence and dual slider existence
- 13/15 E2E tests pass locally (2 fail due to ARM chromium OOM after
heavy live page tests — CI on x64 will handle them)
Closes#119 properly this time.
The previous implementation used L.heatLayer's minOpacity which only
controlled the opacity of the coolest (blue) gradient stops. Now sets
CSS opacity on the canvas element directly, affecting all gradient
colors uniformly. Closes#119 properly.
Customizer now syncs state.home and state.branding to window.SITE_CONFIG
on every change, then dispatches hashchange to trigger page re-render.
Previously only saved to localStorage — home.js reads SITE_CONFIG.
Adds a Theme Presets section at the top of the Theme Colors tab with 8
WCAG AA-verified preset themes:
- Default: Original MeshCore blue (#4a9eff)
- Ocean: Deep blues and teals, professional
- Forest: Greens and earth tones, natural
- Sunset: Warm oranges, ambers, and reds
- Monochrome: Pure grays, no color accent, minimal
- High Contrast: WCAG AAA (7:1), bold colors, accessibility-first
- Midnight: Deep purples and indigos, elegant
- Ember: Dark warm red/orange accents, cyberpunk feel
Each theme has both light and dark variants with all 20 color keys.
High Contrast theme includes custom nodeColors and typeColors for
maximum distinguishability.
Active preset is auto-detected and highlighted. Users can select a
preset then tweak individual colors (becomes Custom).
- Import File button opens file picker for .json theme files
- Merges imported theme into current state, applies live preview
- Also syncs ROLE_COLORS/TYPE_COLORS globals on import
- Moved Copy/Download buttons out of collapsed details
- Raw JSON textarea now editable (was readonly)
- 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)
Added --nav-text and --nav-text-muted CSS variables. All nav
selectors (.top-nav, .nav-brand, .nav-link, .nav-btn, .nav-stats)
use these instead of --text/--text-muted. Nav Text is in Basic
settings. Nav Muted Text in Advanced.
This is separate from page text because nav sits on a dark
background — page text color would be unreadable on the nav.
- Move --status-green/yellow/red from home.css to style.css :root (light+dark)
- Replace hardcoded status colors in style.css (.tl-snr, .health-dot, .byop-err,
.badge-hash-*, .fav-star.on, .spark-fill) with CSS variable references
- Replace hardcoded colors in live.css (VCR mode, stat pills, fdc-link, playhead)
- Replace --primary/--bg-secondary/--text-primary/--text-secondary dead vars with
canonical --accent/--input-bg/--text/--text-muted in style.css, map.js, live.js,
traces.js, packets.js
- Fix nodes.js legend colors to use ROLE_COLORS globals instead of hardcoded hex
- Replace hardcoded hex in home.js (SNR), perf.js (indicators), map.js (accuracy
circles) with CSS variable references via getComputedStyle or var()
- Add --detail-bg to customizer (THEME_CSS_MAP, DEFAULTS, ADVANCED_KEYS, labels)
- Move font/mono out of ADVANCED_KEYS into separate Fonts section in customizer
- Remove debug console.log lines from customize.js
- Bump cache busters in index.html
customize.js loads after roles.js but before app.js triggers
navigate(). Restoring colors in the IIFE body (not DOMContentLoaded)
ensures ROLE_STYLE/ROLE_COLORS/TYPE_COLORS are updated BEFORE
the map or any page creates markers.
Changed nav brand, links, buttons from hardcoded #fff/#cbd5e1 to
var(--text) and var(--text-muted). Setting primary text color
now changes nav text too. Removed unnecessary --nav-text variable.
State now loads: DEFAULTS → server config → localStorage.
Admin saves locally, comes back later, opens customizer —
sees their saved values, export includes everything.
Every color pick, text edit, step change auto-saves (debounced
500ms). No manual save needed. Also fixed syncBadgeColors on
restore and removed stray closing brace.
Textareas use var(--input-bg) and var(--text) instead of white.
Markdown syntax hint shown below each textarea:
**bold** *italic* `code` [text](url) - list
Added miniMarkdown() — simple markdown→HTML (bold, italic, code,
links, lists, line breaks). Home page description/answer fields
render markdown. Customizer uses textareas with markdown hint
for description and answer fields.
Steps/checklist/links were cramming 3+ inputs in one row,
truncating content. Now emoji+title+buttons on row 1,
description on row 2. All content visible.
Badge CSS (.badge-advert etc.) was hardcoded in style.css with
different colors than TYPE_COLORS. Now roles.js generates badge
CSS from TYPE_COLORS on page load via syncBadgeColors(). Customizer
calls syncBadgeColors() after changes. Badges always match the
color pickers and TYPE_COLORS, in both light and dark mode.
Badge classes (.badge-advert etc.) use hardcoded CSS colors.
Now injects a <style> element with color overrides derived from
TYPE_COLORS on every theme preview update.
--content-bg and --card-bg reference --surface-0/--surface-1 via
var() which doesn't live-update when source changes via JS. Now
explicitly sets the derived vars alongside the source.
Color changes dispatch theme-changed event → app.js re-navigates
to current page, rebuilding markers/rows with new colors.
Conflict badges (.hop-ambiguous, .hop-conflict-btn) now use
var(--status-yellow) so they follow the customized status color.
Added global window.TYPE_COLORS in roles.js. Live.js and audio-lab.js
now reference the global. Customizer shows packet type colors with
emoji + descriptions. Changes sync to TYPE_COLORS in real-time.
Saved/restored via localStorage alongside node colors.
Changing node colors in the customizer now updates both ROLE_COLORS
(used for badges, labels) and ROLE_STYLE (used for map markers).
Also fixed localStorage restore to sync both objects.
- Nav bar now uses CSS variables (was hardcoded gradient)
- 19 customizable colors: accent, text, backgrounds, borders,
surfaces, inputs, stripes, hover, selected, status indicators
- Light and dark mode have separate color sets
- Theme tab shows which mode you are editing
- Toggle ☀️/🌙 in nav bar to switch modes and edit the other set
- Export includes both theme and themeDark sections
- localStorage save/restore handles both modes
Export tab now has two sections:
- "My Preferences" — save colors to browser localStorage, auto-applied
on every page load. Personal to you, no server changes.
- "Admin Export" — download config.json for server deployment,
applies to all users.
User theme auto-loads on DOMContentLoaded, overriding CSS variables
and node colors from localStorage.