mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 14:21:30 +00:00
1bfbbd6bb2f3fdb10cfee461dbf16bce7d34da1f
71 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
902f9c4976 |
revert(#1398): nav-instrumentation banner broke page load (#1399)
Reverting PR #1398 — the navdebug banner instrumentation caused pages to hang on load on operator's device. Will respawn safer diagnostic. Refs #1396. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
7f5cc96bd9 |
chore(debug-1396): nav-instrumentation banner — gated on hash ?navdebug=1 (#1398)
## Summary Temporary diagnostic patch for #1396 (mobile / narrow-desktop nav priority reports). Adds a single instrumentation block at the END of `applyNavPriority()` in `public/app.js`, gated on `navdebug=1` appearing in the URL hash. No nav behavior change; reverted once root cause is known. ## What it does When the URL hash contains `navdebug=1` (e.g. `/#/channels?navdebug=1`), the function: 1. Paints a fixed-position green-on-black banner pinned to the bottom of the viewport (`z-index:99999`, `pointer-events:none` so it never blocks interaction) showing: ``` [NAV-DEBUG-1396] vw=<innerWidth> total=N visible=N overflow=N hidden-by-css=N active=<label> visible: [Home,Packets,...] overflow: [Tools,...] ua: <first 80 chars of UA> ``` 2. Emits the same payload via `console.warn('[NAV-DEBUG-1396]', ...)` for anyone who can pop devtools. The whole block is wrapped in `try/catch` — diagnostic code never breaks nav. ## Why a banner (not just console) Affected reporters are on mobile devices where popping devtools is annoying or impossible. A screenshot of the banner gives us: - Viewport width (vs the 768 / 1100 / 1101 breakpoints) - Device UA (Safari iOS quirks, narrow Android, etc.) - Actual link counts after `applyNavPriority` ran - Whether anything is hidden by CSS (`display:none`) despite not being in the overflow set - Which labels are inline vs in the More menu - Active route at time of measurement ## Operator usage On the affected device, open: ``` https://<staging-host>/#/channels?navdebug=1 ``` (or any other route; the gate is hash-wide). Screenshot the green-on-black banner at the bottom of the page and attach to #1396. ## Hard rules respected - Banner is gated — never visible without `navdebug=1` in the hash. - No new dependency. - No change to nav behavior. - Diagnostic-only; revert PR will follow once root cause is identified. ## Out of scope - Root-cause fix for #1396 (this is purely instrumentation). - E2E test for the banner — code is temporary and scheduled for revert. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
f0a7ed758f |
fix(#1391): Priority+ nav — active-route pill must NEVER drop high-priority links into orphaned More dropdown (#1394)
## What
Pins the active-route `.nav-link` inline at any viewport ≥768px so
Priority+ never shoves it into the More dropdown. Fixes the operator's
screenshot of `/#/perf` at ~1080px where the navbar showed only the
active "Perf" pill missing — and an inverse failure where the active
pill was the only thing **in** the dropdown.
This is the 20th regression of nav Priority+. Single-loop fix only; no
algorithm redesign (per issue out-of-scope).
## Root cause
`public/app.js` `applyNavPriority()` had two places that ignored the
active state:
1. **≤1100 narrow-desktop CSS branch (line ~1197):** `if
(a.dataset.priority !== 'high') a.classList.add('is-overflow')` blindly
overflowed every non-high link — including the active pill.
2. **>1100 measurement loop (line ~1267):** `overflowQueue` is `non-high
reversed + high reversed`. The active non-high link enters the queue and
the loop's only break condition is `priority === 'high'`. fits() keeps
returning false (active pill is wider — has the `.active`
background/padding), so the loop walks the entire non-high tail and
orphans the active route in More.
The acceptance criterion "Active-route pill MUST always be visible
inline" was never encoded — #1311's floor only protected
`data-priority="high"`.
## Why prior #1311 / #1148 / #1139 floors didn't catch this
- **#1311** floored at `data-priority="high"` only. `/#/perf` is
`data-priority=""` so it had no protection.
- **#1148 / #1139** floored the *More menu* at ≥2 items but didn't
constrain *which* links could be promoted/dropped.
- **#1106** narrow-desktop CSS branch (≤1100) was written before
active-pill width drift was a known issue.
## Fix
One conceptual rule applied at three points:
1. In `overflowQueue` construction, skip any link with `.active` (treat
active like high-priority — never enqueue).
2. In the ≤1100 CSS branch, skip the active link when assigning
`.is-overflow`.
3. In the >1100 loop, also break on `.active` (defensive — queue already
excludes it).
Approach chosen over "pin active-pill max-width during measurement":
measurement-pinning would silently shrink the pill visually mid-resize,
and width drift from #1378's new `--mc-*` vars made that fragile.
Treating active as a hard inline pin matches the documented contract and
is one greppable invariant.
## TDD red → green
- **Red commit `34d69012`:** added `test-nav-priority-1391-e2e.js`
covering `/#/perf, /#/audio-lab, /#/analytics, /#/observers` at `1024,
1080, 1100, 1101, 1200, 1300px`. Asserts (1) active pill not in
overflow, (2) all 5 high-pri still inline (#1311 guard), (3) every
overflowed link mirrored in More dropdown (no orphans). 0/24 passed
locally on red.
- **Green commit:** same test 24/24 pass. Existing #1311 (20/20), #1139
floor, #1102 contract still green.
## Manual verification
Local fixture server (`./corescope-server -port 13581 -db
test-fixtures/e2e-fixture.db -public public`):
- `/#/perf` @ 1080×800: brand + 5 high-pri inline + "Perf" pill inline +
"More ▾" containing the 5 low-pri links (Channels, Tools, Observers,
Analytics, Audio Lab). ✅
- `/#/perf` @ 1300×800: brand + 5 high-pri + "Perf" inline; More hidden
(only 4 low-pri items overflow). ✅
- `/#/perf` @ 800×800 (narrow): hamburger code path untouched. ✅
- Inverse `/#/home` @ 1080×800 (active IS high-pri): no behaviour
change. ✅
## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— exit 0.
Browser verified: local fixture server + Playwright on Chromium
(`/usr/bin/chromium`).
E2E assertion added: `test-nav-priority-1391-e2e.js:138-148`
(`activeOverflowed === false`).
Fixes #1391
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
9a2270168f |
feat(#893): Material Design dark mode toggle — polished version of #893 (#1389)
## Polished version of #893 This PR carries forward @emuehlstein's Material Design dark-mode toggle from #893, rebased onto current `master` and polished for a11y / first-paint / forced-colors / cross-tab sync. Original commits (preserved as `Co-authored-by`): - `feat: replace dark mode button with Material Design toggle switch` (emuehlstein) - `fix: define --shadow CSS var in theme blocks, drop stopPropagation no-op` (emuehlstein, addressing prior review) #893 had been stuck in CONFLICTING state since 2026-05-24 with no CI runs ever. Rebase resolved a single `public/style.css` `:root` conflict (preserved both the `--text-primary`/`--bg-hover`/`--primary` aliases from #1378 and the new `--shadow` definition). ## Polished improvements (on top of #893) 1. **FOUC fix** (`public/index.html`): inline `<head>` script reads `localStorage('meshcore-theme')` (or `prefers-color-scheme`) and sets `data-theme` *before* stylesheet load. Without this, dark-mode users see a light-mode flash on every page load. 2. **ARIA semantics** (`public/index.html`): moved `aria-label` from the wrapping `<label>` onto the actual `<input role="switch">`. Removed `aria-hidden="true"` from the checkbox (which had been hiding it from assistive tech). Added `aria-hidden` to the decorative track instead. 3. **Keyboard focus indicator** (`public/style.css`): `:focus-visible` on the (visually-hidden) checkbox draws an outline on `.theme-toggle-track`. Previously keyboard users could focus the toggle with Tab but had no visible indicator. 4. **Reduced motion** (`public/style.css`): `@media (prefers-reduced-motion: reduce)` disables the slide/fade transitions. 5. **Forced-colors mode** (`public/style.css`): explicit `CanvasText` border on track + thumb so the switch stays visible in Windows High Contrast. Default CSS tokens collapse to `Canvas`/`CanvasText` and the thumb would otherwise disappear. 6. **Cross-tab sync** (`public/app.js`): `storage` event listener for `meshcore-theme` mirrors the cb-presets pattern from #1378 — toggling theme in one tab now syncs all open tabs. 7. **Tightened E2E test** (`test-e2e-playwright.js`): added assertions for `role="switch"`, checkbox-state ↔ theme parity, and theme persistence across a full page reload (was only asserting one toggle). ## Notes - No `map[string]interface{}` (no Go changes). - All colors via existing `--mc-*` / theme tokens; `--shadow` is defined in both light + dark theme blocks. - No layout shift (track is fixed `46x24` inside the `44x44` label container). - Branch scope is exactly the four files from #893: `public/app.js`, `public/index.html`, `public/style.css`, `test-e2e-playwright.js`. Closes #893. Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com> --------- Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com> Co-authored-by: CoreScope Bot <bot@corescope> |
||
|
|
101c11b4b3 |
fix(#1361): theme customizer — colorblind presets [WIP] (#1378)
WIP — draft PR for CI to exercise the RED test commit. Will be promoted
out of draft once the GREEN commit lands.
Red commit:
|
||
|
|
96a79ce9c1 |
fix(nav): floor Priority+ overflow at high-priority links — fixes nav vanishing on non-high routes (#1311) (#1312)
Red commit: `5f366b71` — CI: pending (will link once first run starts). Fixes #1311 ## The bug `applyNavPriority` in `public/app.js` had no floor on the iterative overflow loop: ```js let i = 0; while (!fits() && i < overflowQueue.length) { overflowQueue[i].classList.add('is-overflow'); i++; } ``` The `overflowQueue` is built non-high-first then high-priority tail. When `fits()` kept returning `false` — because the active-route pill renders wider than other links — the loop walked past the non-high tail and started dropping high-priority links too. On a non-high active route (`/#/perf`, `/#/audio-lab`, `/#/analytics`, `/#/observers`) at ~1101–1200px, this nuked Home/Packets/Map/Live/Nodes and left the user with brand + "More ▾" + the active pill. ## Repro (master) 1. `go build ./cmd/server` and serve against the e2e fixture 2. Visit `http://localhost:13581/#/perf` at 1101px viewport 3. Inline strip shows only "More ▾" + the ⚡ Perf pill — Home/Packets/Map/Live/Nodes are all gone 4. New E2E (`test-nav-priority-1311-e2e.js`) reproduces this: 4/16 cases fail at 1101px on master. ## The fix Two-line floor in the loop guard: break when the next queue item is a high-priority link. ```js while (!fits() && i < overflowQueue.length) { if (overflowQueue[i].dataset.priority === 'high') break; overflowQueue[i].classList.add('is-overflow'); i++; } ``` The `>=2` More-menu floor (#1139) gets the same guard — never promote a high-priority link just to hit the floor. A degenerate 1-item dropdown is a smaller paper-cut than nuking primary nav. ## TDD trail - **RED commit `5f366b71`**: `test-nav-priority-1311-e2e.js` lands first. Asserts (`assert.deepStrictEqual`) all 5 high-priority hrefs are visible inline at 900/1024/1101/1200px on /#/perf, /#/audio-lab, /#/analytics, /#/observers (16 cases). Fails 4/16 against master. - **GREEN commit `6d1a5542`**: floor added; 16/16 pass. Existing nav suite still green: - `test-nav-priority-1102-e2e.js`: 5/5 ✅ - `test-nav-more-floor-1139-e2e.js`: 10/10 ✅ - `test-nav-fluid-1055-e2e.js`: 20/20 ✅ - **Mutation guard**: stash the floor → test fails 4/16 again on the same cases. Browser verified: chromium 136 against local Go server with `test-fixtures/e2e-fixture.db` at 900/1024/1101/1200px on each non-high route. E2E assertion added: `test-nav-priority-1311-e2e.js:107` (`assert.deepStrictEqual`). ## Constraints respected - Existing 5/5 inline behavior on /#/home (active route IS high-priority) — preserved by 1102 suite ✅ - `<=1100` branch — unchanged (already data-priority-aware) ✅ - `>=2` More-menu floor (#1139) — preserved + extended with the same high-pri guard ✅ - All colors via CSS vars ✅ - PII preflight clean ✅ --------- Co-authored-by: CoreScope Bot <bot@corescope> |
||
|
|
f5785e89f4 |
fix(traces): fix path graph legibility and overlapping edges (#1134)
## Summary - Drop prefix-only paths from path graph: partial observations (same packet seen at 1, 2, 4, 5 hops as it propagated) were treated as separate routes, producing long shortcut edges to Dest that visually obscured the actual relay chain. Now filters out any path that is a strict prefix of a longer observed path before building the graph. - Fix invisible node labels: intermediate hop nodes used white text on `--surface-2` background, making labels invisible in the light theme. Labels now appear below circles and use `var(--text)` for theme-aware contrast. Increased SVG height and node radius to give labels room; intermediate fill uses a subtle accent tint with accent border. ## Test plan - [ ] Open a TRACE packet's path graph with a node that has multiple partial observations — verify no spurious shortcut edges - [ ] Check path graph in light theme — verify intermediate hop labels are visible - [ ] Check path graph in dark theme — verify no regression 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ab34d9fb65 |
fix(#1206): keep VCR bar from occluding the live packet feed (#1213)
Red commit: `bcfc74de` (CI: https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1206) Fixes #1206. ## Problem On Live Map the VCR (timeline/playback) bar overlays the bottom of the viewport. Bottom-pinned overlays — the live packet feed, the legend, any corner panel — used hard-coded `bottom: 58–88px` offsets that are smaller than the real bar height (two-row mobile layout + `env(safe-area-inset-bottom)` push it to ~80px and beyond). The last N packet-feed rows slid under the bar and became unreadable / unclickable. ## Fix Publish the bar's measured height as a CSS variable on the live page and bind every bottom-anchored overlay to it. - `public/live.js` — new `initVCRHeightTracker()` runs after init; uses `ResizeObserver` + `resize` / `visualViewport.resize` to keep `--vcr-bar-height` on `.live-page` in sync with `#vcrBar`. - `public/live.css` — `.live-feed`, `.feed-show-btn`, and the `.live-overlay[data-position="bl"|"br"]` corner slots now use `bottom: calc(var(--vcr-bar-height, 58px) + 10px)`. The feed's `max-height` is also capped against `100dvh - top - vcr - margin` so its scroll container can never extend past the bar. - Stale per-breakpoint overrides (the `@supports(env(safe-area-inset))` hard-coded `78px + safe-area` for feed/legend) are removed in favor of the single tracked variable. ## TDD - Red commit `bcfc74de` adds `test-issue-1206-vcr-overlap-e2e.js`: asserts `#liveFeed.getBoundingClientRect().bottom <= #vcrBar.top` (and same for the last row) at desktop 1280x800 and mid 720x800. Verified locally that reverting the green commit makes the feed-bottom assertions fail (feed bottom 742px > VCR top 721px) — see PR body for exact numbers from the local run. - Green commit `1ad17e7f` makes all 5 assertions pass. ## Browser verified Local Go server with `test-fixtures/e2e-fixture.db`, headless Chromium via the new E2E test — all 5 assertions green. ## E2E assertion added `test-issue-1206-vcr-overlap-e2e.js:84` (bottom-row vs VCR-top) plus container check at `:74`. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <bot@corescope.local> |
||
|
|
9d1f5d2395 |
fix(#1061): bottom navigation for narrow viewports (#1174)
Red commit: a200704d5e27e47c0b29a4745bf1a1772a8876fe (CI URL added once Actions resolves the run) Fixes #1061 ## What Bottom navigation at ≤768px with 5 tabs in spec order: Home, Packets, Live, Map, Channels. Top-nav suppressed at the same breakpoint — no duplicate nav UX. ## Files - NEW `public/bottom-nav.js` — renders 5 tabs, syncs `.active` on `hashchange`, reuses the existing in-app hash router (`<a href="#/...">`). Stable selector `[data-bottom-nav-tab="<route>"]`. Container `[data-bottom-nav]`. - NEW `public/bottom-nav.css` — styles. Tokens reused: `--nav-bg`, `--nav-text`, `--nav-text-muted`, `--nav-active-bg`, `--accent`, `--border` (all global → resolve in BOTH light and dark themes). - `public/index.html` — one `<link>` for the CSS, one `<script>` after `app.js`. The `<nav>` is appended by JS as a sibling of `<main id="app">` at DOMContentLoaded. - `test-bottom-nav-1061-e2e.js` + `.github/workflows/deploy.yml` — Playwright wiring. ## Decisions - **Breakpoint:** `@media (max-width: 768px)`. No `@container` rules exist anywhere in `style.css` today — media query is consistent. - **Top-nav suppression:** `display:none` at ≤768px. Simpler than a hamburger collapse; long-tail routes (Tools/Lab/Perf) remain reachable by URL; "More"-tab/hamburger fallback deferred per issue body. - **Active indicator:** `var(--nav-active-bg)` + 2px accent top-border. No moving pill. - **Safe-area:** `padding-bottom: env(safe-area-inset-bottom)` on nav + reciprocal `body` reservation. `viewport-fit=cover` already in place. - **Reduced motion:** `prefers-reduced-motion: reduce` disables the transition. ## TDD - Red: `a200704` — assertions fail (no bottom-nav). - Green: `53851a1` — component + styles. E2E assertion added: `test-bottom-nav-1061-e2e.js:71` (case (a) — bottom-nav visible at 360x800). --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
05876b3a59 |
fix(#1173): replace #liveDot with packet-driven brand-logo node-pulse (#1177)
Red commit: PENDING (will update) Fixes #1173. Replaces the `#liveDot` WebSocket-connected indicator with a packet-driven node-pulse animation on the brand logo's two inner circles. ## Behavior (locked per issue spec) - **Animation curve:** `ease-out` (default per open-question 1). - **Rate cap:** 15/sec (66ms gap; default per open-question 2). Excess triggers are dropped, never queued. - **Direction:** alternates A→B / B→A across messages (aesthetic, not semantic). - **Idle ≥10s:** logo at full brightness, no animation. - **Disconnected:** `.logo-disconnected` applies `filter: grayscale(0.6) opacity(0.7)`. - **`prefers-reduced-motion: reduce`:** single-step `.logo-pulse-blip` on destination only. ## Implementation - WS handler hook lives in `public/app.js` `connectWS()` (`ws.onmessage` triggers `Logo.pulse()`; `ws.onopen`/`ws.onclose` toggle `Logo.setConnected()`). - `Logo` is a small IIFE in `app.js` that exposes `window.__corescopeLogo` for E2E injection. - All animation is pure CSS; JS only toggles `.logo-pulse-active` / `.logo-pulse-blip` / `.logo-disconnected`. Colors come exclusively from `--logo-accent` / `--logo-accent-hi` tokens. - Two new classes (`.logo-node-a`, `.logo-node-b`) attached to inner circles in both `.brand-logo` and `.brand-mark-only` SVGs so the mobile mark animates too. ## `#liveDot` removal proof ``` $ grep -rn liveDot public/ (no output) ``` ## E2E - E2E assertion added: `test-logo-pulse-1173-e2e.js:54` and follows. - Wired into the Playwright matrix in `.github/workflows/deploy.yml` (mirrors PR #1168 pattern from commit `5442652`). - Test injects synthetic pings via `window.__corescopeLogo.pulse({ synthetic: true })`; matches the existing harness style (no new WS-mock pattern invented). Red→green discipline preserved: the test commit lands first and CI fails on assertion; the implementation commit follows. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
81f95aaabe |
fix(nav): floor More menu at >=2 items (#1139 Bug B) (#1148)
Partial fix for #1139 — closes Bug B (desktop More menu degenerate). Bug A (mobile hamburger) blocked on user device info; left for separate PR. ## What this changes `public/app.js` `applyNavPriority()` (the >1100px measurement branch): add a "minimum More menu size" floor. After the greedy `fits()` loop terminates, if exactly one link ended up in `is-overflow`, promote one more from the overflow queue so the dropdown contains ≥2 items. ```diff let i = 0; while (!fits() && i < overflowQueue.length) { overflowQueue[i].classList.add('is-overflow'); i++; } + // #1139 Bug B: floor the More menu at >=2 items. + var overflowedCount = allLinks.filter(a => a.classList.contains('is-overflow')).length; + if (overflowedCount === 1 && i < overflowQueue.length) { + overflowQueue[i].classList.add('is-overflow'); + i++; + } rebuildMoreMenu(); ``` The ≤1100px Priority+ design contract (5 high-priority + More) is unchanged; the floor only applies on the measurement branch. ## Why Above 1100px the measurement loop greedily fills inline links until something overflows. If exactly one non-priority link is wider than the remaining slack, the loop pushes only it into overflow and stops — producing a one-item "More ▾" dropdown. With the fixture stats this reproduces deterministically at 1600px (overflow=`["🎵 Lab"]`); the prod report on 1101–1278px is the same root cause with realistic `#navStats` width consuming most of the remaining slack. ## TDD - Red: `test-nav-more-floor-1139-e2e.js` sweeps 1101, 1150, 1200, 1240, 1278, 1280, 1340, 1500, 1600, 1700px and asserts `#navMoreMenu.children.length` is 0 or ≥2 — never 1. On master it fails at 1600px (`items=1, overflow=[#/audio-lab]`). - Green: with the floor in place all 10 viewports pass. - Existing `test-nav-priority-1102-e2e.js` and `test-nav-fluid-1055-e2e.js` still pass (5/5 and 20/20). - Wired into CI alongside the other nav E2E tests. ## Out of scope (Bug A) The mobile hamburger inert-button report needs a console snapshot from the affected device (pasted in the issue body) to pin the root cause. Left open for a follow-up PR. This PR uses "Partial fix" intentionally and does NOT include `Fixes #1139` so the issue stays open. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> |
||
|
|
bf68a99acf |
polish: nav Priority+ hardening + tighter E2E (Fixes #1105) (#1106)
Fixes #1105. Polish follow-ups from #1104's independent review (https://github.com/Kpa-clawbot/CoreScope/pull/1104#issuecomment-4381850096). All 9 MINORs addressed. ## Hardening (`public/app.js`, commit |
||
|
|
c84ec409c7 |
fix(nav): #1102 — JS-driven Priority+ measures actual fit (#1104)
## Summary Fixes #1102 — regression from PR #1097 polish where Priority+ collapsed too aggressively at wide widths and the "More" menu didn't reflect what was actually hidden. ## Root cause Two bugs, one root: the post-#1097 CSS rule ```css .nav-links a:not([data-priority="high"]) { display: none; } ``` unconditionally hid 6 of 11 links at every width ≥768px — including 2560px where everything fits comfortably. The "More" menu populator (`querySelectorAll('.nav-links a:not([data-priority="high"])')`) ran exactly once on load against that same selector, so it always held the same 6 links and never reflected the actual viewport fit. ## Fix Replace the static CSS hide with a JS measurement pass (`applyNavPriority` in `public/app.js`): 1. Start with **all** links visible inline. 2. Compute `needed = brand + gutters + visible-links + More + nav-right + safety` and compare to `window.innerWidth`. 3. If it doesn't fit, mark the rightmost (lowest-priority) link `.is-overflow` and re-measure. Repeat. High-priority links are queued last so they're kept visible as long as possible. 4. Rebuild the "More ▾" menu from whatever currently has `.is-overflow`. When nothing overflows, hide the More wrap entirely (`.is-hidden`). 5. Re-run on resize (rAF-debounced), on `hashchange` (active-link padding shifts), and after fonts load. Why JS, not CSS: the breakpoint where each link drops depends on label width, gutters, active-link padding, and the nav-stats badge — none of which are addressable from a media query. ## TDD trail - Red commit `8507756`: `test-nav-priority-1102-e2e.js` — fails 2/4 (2560 and 1920 only show 5/11). - Green commit `3e84736`: implementation — passes 4/4. ## E2E `test-nav-priority-1102-e2e.js` asserts: - 2560px → all 11 visible, More hidden - 1920px → ≥9 visible - 1080px → 5+ visible AND More menu contains every hidden link - 800px → 5+ visible AND More menu non-empty Local run on the e2e fixture: **4/4 pass**. Existing `test-nav-fluid-1055-e2e.js` also stays green: **20/20 pass** (no overlap, no overflow at 768/1024/1280/1440/1920 across 4 routes). --------- Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
88dca33355 |
fix(touch): tune pull-to-reconnect to require deliberate pull (#1091) (#1092)
## Summary Fixes #1091 — pull-to-reconnect was triggering on normal scrolling because the threshold was too low and `preventDefault` fired too early. ## Changes **`public/app.js`** — `setupPullToReconnect()` gesture tuning: | Behavior | Before | After | |---|---|---| | Threshold | 80px | **140px** (deliberate pull, not bounce) | | `preventDefault` fires at | 16px (kills native scroll feel) | **140px** (only after commit) | | scrollTop check | `> 0` (allowed negative overscroll) | **strict `=== 0`** | | Mid-gesture scroll | continued tracking | **cancels gesture** | | `touchend` scrollTop check | none | **must still be 0** | ## TDD evidence - Red commit: `bcf0d79` — added `test-pull-to-reconnect-1091.js`. The "100px pull at scrollTop=0: NO reconnect" assertion fails on master because the old 80px threshold triggers there. Six other gesture-tuning assertions also gated. - Green commit: `4071dd0` — production fix. All 7 new tests + 6 existing pull-to-reconnect tests pass. ## E2E coverage (per acceptance criteria) - 50px pull → no trigger - 100px pull → no trigger (regression guard against old 80px threshold) - 160px pull → triggers - Pull from non-zero scrollTop → no trigger - Lift before threshold → no trigger - scrollTop changes from 0 mid-pull → cancels - preventDefault not called below threshold E2E assertion added: `test-pull-to-reconnect-1091.js:154` (the 100px regression-guard assertion that demonstrates the bug fix). ## Test results ``` test-pull-to-reconnect-1091.js: 7 passed, 0 failed test-pull-to-reconnect.js: 6 passed, 0 failed ``` Fixes #1091 --------- Co-authored-by: clawbot <bot@openclaw.local> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
36ee71d17e |
feat(#1085): fold Roles page into Analytics tab (#1088)
Red commit:
|
||
|
|
cbfd159f8e |
feat(ws): pull-to-reconnect on touch devices (Fixes #1063) (#1068)
## Summary Reframes the browser's native pull-to-refresh on touch devices as a **WebSocket reconnect** instead of a full page reload. On data pages (Packets, Nodes, Channels — and globally, since the WS is shared) a downward pull at `scrollTop=0` cycles the WS, which is what users actually want when they reach for that gesture. Fixes #1063. ## Behavior - **Touch-only**: gated by `('ontouchstart' in window) || navigator.maxTouchPoints > 0`. Desktop is untouched. - **Scroll-safe**: every handler re-checks `scrollTop > 0` and bails out — never hijacks normal scroll. - **Visual affordance**: a fixed chip slides down from the top with a rotating ⟳ icon; opacity and rotation scale with pull progress (0 → `PULL_THRESHOLD_PX = 80px`). - **`preventDefault` is conservative**: only after `dy > 16px` and only on `touchmove`, so taps and short swipes are not affected. - **Result feedback**: a brief toast — green `Connected ✓` if WS was already OPEN, `Reconnecting…` otherwise. Both auto-dismiss after ~1.8s. - **Reconnect path**: closes the existing WS so the existing `onclose` auto-reconnect fires immediately; an explicit `connectWS()` is also called as a safety net when `ws` is null. - **No regression** to existing WS auto-reconnect — same `connectWS` / `setTimeout(connectWS, 3000)` chain, just kicked manually. ## TDD - **Red commit** `f90f5e9` — adds `test-pull-to-reconnect.js` with 6 assertions; stub functions added to `app.js` so tests reach assertion failures (not ReferenceError). 3/6 fail on behavior. - **Green commit** `53adbd9` — full implementation; 6/6 pass. ## Files - `public/app.js` — `pullReconnect()`, `setupPullToReconnect()`, `_ensurePullIndicator()`, `_showPullToast()`, `_isTouchDevice()`. Wired into `DOMContentLoaded` next to `connectWS()`. Touched the WS section only. - `test-pull-to-reconnect.js` — vm sandbox suite covering exposure, WS-close, listener wiring, threshold trigger, scroll-position gate. ## Acceptance criteria check - ✅ Pull-down at scroll-top triggers WS reconnect + data refetch (debounced cache invalidate fires on next WS message) - ✅ Visible affordance during pull (rotating chip) - ✅ Resolves on success (toast), shows status toast on disconnect path - ✅ Disabled when not at `scrollTop=0` - ✅ No regression to existing WS auto-reconnect --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> |
||
|
|
736b09697d |
fix(analytics): apply customizer timestamp format to chart axes (closes #756) (#981)
## Summary Fixes #756 — the customizer timestamp format setting (ISO/ISO+ms/locale) and timezone (UTC/local) were not applied to chart X-axis labels, tooltips, or certain inline timestamps in the analytics pages. ## Changes ### `public/app.js` - Added `formatChartAxisLabel(date, shortForm)` — a shared helper that reads the customizer's `timestampFormat` and `timestampTimezone` preferences and formats dates for chart axes accordingly. `shortForm=true` returns time-only (for intra-day charts), `shortForm=false` returns date+time (for multi-day ranges). ### `public/analytics.js` - `rfXAxisLabels()`: now calls `formatChartAxisLabel()` instead of hardcoded `toLocaleTimeString()` - `rfTooltipCircles()`: tooltip timestamps now use `formatAbsoluteTimestamp()` instead of raw ISO - Subpath detail first/last seen: now uses `formatAbsoluteTimestamp()` - Neighbor graph last_seen: now uses `formatAbsoluteTimestamp()` ### `public/node-analytics.js` - Packet timeline chart labels: now use `formatChartAxisLabel()` (respects short vs long form based on time range) - SNR over time chart labels: now use `formatChartAxisLabel()` ## Behavior by setting | Setting | Chart axis (short) | Chart axis (long) | |---------|-------------------|-------------------| | ISO | `14:30` | `05-03 14:30` | | ISO+ms | `14:30:05` | `05-03 14:30:05` | | Locale | `2:30 PM` | `May 3, 2:30 PM` | All respect the UTC/local timezone toggle. ## Testing - Server builds cleanly (`go build`) - Served `app.js` contains `formatChartAxisLabel` (verified via curl) - Graceful fallback: all callsites check `typeof formatChartAxisLabel === 'function'` before calling, preserving backward compat if script load order changes --------- Co-authored-by: you <you@example.com> |
||
|
|
4f0f7bc6dd |
fix(ui): fill remaining gaps in payload-type lookup tables (10/11/15) (#967)
## Summary Fill the remaining gaps in payload-type lookup tables noted out-of-scope on #965. Every firmware-defined payload type (0–11, 15) now has entries in all four frontend tables. ## Changes Three types were missing from one or more tables: | Type | Name | `PAYLOAD_COLORS` (app.js) | `TYPE_NAMES` (packets.js) | `TYPE_COLORS` (roles.js) | `TYPE_BADGE_MAP` (roles.js) | |------|------|--------------------------|--------------------------|-------------------------|---------------------------| | 10 | Multipart | added | added | added `#0d9488` | added | | 11 | Control | added | ✅ (already) | added `#b45309` | added | | 15 | Raw Custom | added | added | added `#c026d3` | added | ## Color choices - **MULTIPART** `#0d9488` (teal) — multi-fragment stitching, distinct from PATH's `#14b8a6` - **CONTROL** `#b45309` (amber) — warm brown, distinct hue from ACK's grey `#6b7280` - **RAW_CUSTOM** `#c026d3` (fuchsia) — magenta, distinct from TRACE's pink `#ec4899` All pass WCAG 3:1 contrast against both white and dark (#1e1e1e) backgrounds. ## Tests - `test-packets.js`: 82/82 ✅ - `test-hash-color.js`: 32/32 ✅ Badge CSS auto-generation: `syncBadgeColors()` in `roles.js` iterates `TYPE_BADGE_MAP` keyed against `TYPE_COLORS`, so the three new entries automatically get `.type-badge.multipart`, `.type-badge.control`, and `.type-badge.raw-custom` CSS rules injected at page load. Firmware source: `firmware/src/Packet.h:19-32` — types 0x00–0x0B and 0x0F. Types 0x0C–0x0E are not defined. Follows up on #965. --------- Co-authored-by: you <you@example.com> |
||
|
|
c67f3347ce |
fix(ui): add GRP_DATA (type 6) to filter dropdown + color tables (#965)
## Bug Packet type 6 (`PAYLOAD_TYPE_GRP_DATA` per `firmware/src/Packet.h:25`) was missing from three frontend lookup tables: - `public/app.js:7` — `PAYLOAD_COLORS` had no entry for 6 → badge color fell back to `unknown` (grey) - `public/packets.js:29` — `TYPE_NAMES` (used by the Packets page type-filter dropdown) had no entry for 6 → "Group Data" missing from the menu - `public/roles.js:17,24` — `TYPE_COLORS` and `TYPE_BADGE_MAP` had no `GRP_DATA` entry → no dedicated CSS class The packet detail page already handled it (via `PAYLOAD_TYPES` in `app.js:6` which had `6: 'Group Data'`) so individual GRP_DATA packets render correctly. The gap was only in the filter UI + badge styling. ## Fix Add the missing entry in each table. 4 lines across 3 files. - `app.js`: add `6: 'grp-data'` to `PAYLOAD_COLORS` - `packets.js`: add `6:'Group Data'` to `TYPE_NAMES` - `roles.js`: add `GRP_DATA: '#8b5cf6'` to `TYPE_COLORS` and `GRP_DATA: 'grp-data'` to `TYPE_BADGE_MAP` Color choice `#8b5cf6` (violet) — distinct from GRP_TXT's blue but visually adjacent so operators read them as related types. ## Verification (rule 18 + 19) Built server locally, served the JS files, grepped the rendered output: ``` $ curl -s http://localhost:13900/packets.js | grep TYPE_NAMES const TYPE_NAMES = { ... 5:'Channel Msg', 6:'Group Data', 7:'Anon Req' ... }; $ curl -s http://localhost:13900/app.js | grep PAYLOAD_TYPES const PAYLOAD_TYPES = { ... 5: 'Channel Msg', 6: 'Group Data', 7: 'Anon Req' ... }; $ curl -s http://localhost:13900/roles.js | grep GRP_DATA ADVERT: '#22c55e', GRP_TXT: '#3b82f6', GRP_DATA: '#8b5cf6', ... ADVERT: 'advert', GRP_TXT: 'grp-txt', GRP_DATA: 'grp-data', ... ``` Frontend tests pass: `test-packets.js` 82/82, `test-hash-color.js` 32/32. ## Out of scope Consolidating the duplicated PAYLOAD_TYPES / TYPE_NAMES tables into a single source of truth is a separate cleanup. Two parallel name maps continues to be a footgun (this is the second time a new type's been added to one but not the other). Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
053aef1994 |
fix(spa): decouple navigate() from theme fetch + add data-loaded sync attributes (#955) (#958)
## Summary Fixes the chained async init race identified in RCA #3 of #955. `navigate()` (which dispatches page handlers and fetches data) was gated behind `/api/config/theme` resolving via `.finally()`. Tests use `waitUntil: 'domcontentloaded'` which returns BEFORE theme fetch resolves, creating a race condition where 3+ serial network requests must complete before any DOM rows appear. ## Changes ### Decouple navigate() from theme fetch (public/app.js) - Move `navigate()` call out of the theme fetch `.finally()` block - Call it immediately on DOMContentLoaded — theme is purely cosmetic and applies in parallel ### Add data-loaded sync attributes (public/nodes.js, map.js, packets.js) - Set `data-loaded="true"` on the container element after each page's data fetch resolves and DOM renders - Nodes: set on `#nodesLeft` after `loadNodes()` renders rows - Map: set on `#leaflet-map` after `renderMarkers()` completes - Packets: set on `#pktLeft` after `loadPackets()` renders rows ### Update E2E tests (test-e2e-playwright.js) - Add `await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 })` before row/marker assertions - Increase map marker timeout from 3s to 8s as additional safety margin - Tests now synchronize on data readiness rather than racing DOM appearance ## Verification - Spun up local server on port 13586 with e2e-fixture.db - Confirmed navigate() is called immediately (not gated on theme) - Confirmed data-loaded attributes are present in served JS - API returns data correctly (2 nodes from fixture) Closes #955 (RCA #3) Co-authored-by: you <you@example.com> |
||
|
|
54f7f9d35b |
feat: path-prefix candidate inspector with map view (#944) (#945)
## feat: path-prefix candidate inspector with map view (#944) Implements the locked spec from #944: a beam-search-based path prefix inspector that enumerates candidate full-pubkey paths from short hex prefixes and scores them. ### Server (`cmd/server/path_inspect.go`) - **`POST /api/paths/inspect`** — accepts 1-64 hex prefixes (1-3 bytes, uniform length per request) - Beam search (width 20) over cached `prefixMap` + `NeighborGraph` - Per-hop scoring: edge weight (35%), GPS plausibility (20%), recency (15%), prefix selectivity (30%) - Geometric mean aggregation with 0.05 floor per hop - Speculative threshold: score < 0.7 - Score cache: 30s TTL, keyed by (prefixes, observer, window) - Cold-start: synchronous NeighborGraph rebuild with 2s hard timeout → 503 `{retry:true}` - Body limit: 4096 bytes via `http.MaxBytesReader` - Zero SQL queries in handler hot path - Request validation: rejects empty, odd-length, >3 bytes, mixed lengths, >64 hops ### Frontend (`public/path-inspector.js`) - New page under Tools route with input field (comma/space separated hex prefixes) - Client-side validation with error feedback - Results table: rank, score (color-coded speculative), path names, per-hop evidence (collapsed) - "Show on Map" button calls `drawPacketRoute` (one path at a time, clears prior) - Deep link: `#/tools/path-inspector?prefixes=2c,a1,f4` ### Nav reorganization - `Traces` nav item renamed to `Tools` - Backward-compat: `#/traces/<hash>` redirects to `#/tools/trace/<hash>` - Tools sub-routing dispatches to traces or path-inspector ### Store changes - Added `LastSeen time.Time` to `nodeInfo` struct, populated from `nodes.last_seen` - Added `inspectMu` + `inspectCache` fields to `PacketStore` ### Tests - **Go unit tests** (`path_inspect_test.go`): scoreHop components, beam width cap, speculative flag, all validation error cases, valid request integration - **Frontend tests** (`test-path-inspector.js`): parse comma/space/mixed, validation (empty, odd, >3 bytes, mixed lengths, invalid hex, valid) - Anti-tautology gate verified: removing beam pruning fails width test; removing validation fails reject tests ### CSS - `--path-inspector-speculative` variable in both themes (amber, WCAG AA on both dark/light backgrounds) - All colors via CSS variables (no hardcoded hex in production code) Closes #944 --------- Co-authored-by: you <you@example.com> |
||
|
|
6ca5e86df6 |
fix: compute hex-dump byte ranges client-side from per-obs raw_hex (#891)
## Symptom The colored byte strip in the packet detail pane is offset from the labeled byte breakdown below it. Off by N bytes where N is the difference between the top-level packet's path length and the displayed observation's path length. ## Root cause Server computes `breakdown.ranges` once from the top-level packet's raw_hex (in `BuildBreakdown`) and ships it in the API response. After #882 we render each observation's own raw_hex, but we keep using the top-level breakdown — so a 7-hop top-level packet shipped "Path: bytes 2-8", and when we rendered an 8-hop observation we coloured 7 of the 8 path bytes and bled into the payload. The labeled rows below (which use `buildFieldTable`) parse the displayed raw_hex on the client, so they were correct — they just didn't match the strip above. ## Fix Port `BuildBreakdown()` to JS as `computeBreakdownRanges()` in `app.js`. Use it in `renderDetail()` from the actually-rendered (per-obs) raw_hex. ## Test Manually verified the JS function output matches the Go implementation for FLOOD/non-transport, transport, ADVERT, and direct-advert (zero hops) cases. Closes nothing (caught in post-tag bug bash). --------- Co-authored-by: you <you@example.com> |
||
|
|
20843979a7 |
fix(#861): restore sticky table headers on mobile packets page (#867)
## What Remove a single line in `makeColumnsResizable()` that set `th.style.position = 'relative'` on every `<th>` except the last, overriding the CSS `position: sticky` rule from `.data-table th`. ## Why The column-resize feature added inline `position: relative` to each header (except the last) to serve as a containing block for the absolute-positioned resize handles. This inadvertently broke `position: sticky` on all headers except "Details" (the last column) — visible on mobile when scrolling the packets table. `position: sticky` is itself a positioned value and serves as a containing block for absolute children, so the resize handles work identically without the override. ## Test - Open `/#/packets` on mobile (or narrow viewport) - Scroll down — ALL column headers now remain sticky at the top - Column resize handles still function correctly on desktop Fixes #861 Co-authored-by: you <you@example.com> |
||
|
|
3630a32310 |
fix(#852): transport-route path_len offset + var(--muted) → var(--text-muted) (#853)
## Problem Two pre-existing bugs found during expert review of #851: ### 1. `hashSize` derivation ignores transport route types `public/packets.js` hardcoded path-length byte at offset 1: ```js const rawPathByte = pkt.raw_hex ? parseInt(pkt.raw_hex.slice(2, 4), 16) : NaN; ``` For transport routes (`route_type` 0 DIRECT or 3 TRANSPORT_ROUTE_FLOOD), bytes 1–4 are `next_hop` + `last_hop` and path-length is at offset 5. Same bug #846 fixed inside the byte-breakdown function. ### 2. `var(--muted)` CSS variable is undefined Used in 6 places in `public/packets.js`. No `--muted` variable is defined anywhere in `public/*.css` — only `--text-muted` exists. Text styled with `var(--muted)` silently falls through to inherited color, making badges/hints invisible. ## Fix ### Fix 1: transport-route path_len offset ```js const plOff = (pkt.route_type === 0 || pkt.route_type === 3) ? 5 : 1; const rawPathByte = pkt.raw_hex ? parseInt(pkt.raw_hex.slice(plOff * 2, plOff * 2 + 2), 16) : NaN; ``` ### Fix 2: `var(--muted)` → `var(--text-muted)` All 6 occurrences replaced. ## Tests (5 new, 572 total) - `hashSize` extraction for flood route (route_type=1, offset 1) - `hashSize` extraction for direct transport route (route_type=0, offset 5) - `hashSize` extraction for transport route flood (route_type=3, offset 5) - `hashSize` returns null for missing raw_hex - Regression guard: no `var(--muted)` in any `public/` JS/CSS file ## Changes - `public/packets.js`: 7 lines changed (1 offset fix + 6 CSS var fixes) - `test-frontend-helpers.js`: 46 lines added (5 tests) Closes #852 --------- Co-authored-by: you <you@example.com> |
||
|
|
bd54707987 |
feat: distance unit preference — km, mi, or auto (#621) (#646)
## Summary - **`app.js`**: `getDistanceUnit()`, `formatDistance(km)`, `formatDistanceRound(km)` helpers. Auto mode uses `navigator.language` — miles for `en-US`, `en-GB`, `my`, `lr`; km everywhere else. - **`customize-v2.js`**: Distance Unit preference (km / mi / auto) in Display Settings panel. Stored in `localStorage['meshcore-distance-unit']` via the existing apply pipeline. Override dot and reset work. Display tab badge counts it. - **`nodes.js`**: Neighbor table distance cell uses `formatDistance()`. - **`analytics.js`**: All rendered km values use `formatDistance()` or `formatDistanceRound()`. Column headers (`km`/`mi`) respond to the active unit. Collision classification thresholds (Local < 50 km / Regional 50–200 km / Distant > 200 km) also adapt. Default is `auto` — no change for existing users unless their locale maps to miles. ## Test plan - [x] `node test-frontend-helpers.js` — 456 passed, 0 failed (10 new formatDistance tests) - [ ] Set unit to **mi** in customize → Neighbors table shows `7.6 mi` instead of `12.3 km` - [ ] Analytics → Distance tab → stat cards, leaderboard, and column headers all show miles - [ ] Collision tool → Local/Regional/Distant thresholds show `31 mi` / `124 mi` - [ ] Route patterns popup shows miles per hop and total - [ ] Reset override dot → unit returns to auto Closes #621 🤖 Generated with [Claude Code](https://claude.ai/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: you <you@example.com> |
||
|
|
2bff89a546 |
feat: deep link P1 UI states — nodes tab, packets filters, channels node panel (#536) (#618)
## Summary
- **nodes.js**: `#/nodes?tab=repeater` and `#/nodes?search=foo` — role
tab and search query are now URL-addressable; state resets to defaults
on re-navigation
- **packets.js**: `#/packets?timeWindow=60` and
`#/packets?region=US-SFO` — time window and region filter survive
refresh and are shareable
- **channels.js**: `#/channels/{hash}?node=Name` — node detail panel is
URL-addressable; auto-opens on load, URL updates on open/close
- **region-filter.js**: adds `RegionFilter.setSelected(codesArray)` to
public API (needed for URL-driven init)
All changes use `history.replaceState` (not `pushState`) to avoid
polluting browser history. URL params override localStorage on load;
localStorage remains fallback.
## Implementation notes
- Router strips query string before computing `routeParam`, so all pages
read URL params directly from `location.hash`
- `buildNodesQuery(tab, searchStr)` and `buildPacketsUrl(timeWindowMin,
regionParam)` are pure functions exposed on `window` for testability
- Region URL param is applied after `RegionFilter.init()` via a
`_pendingUrlRegion` module-level var to keep ordering explicit
- `showNodeDetail` captures `selectedHash` before the async `lookupNode`
call to avoid stale URL construction
## Test plan
- [x] `node test-frontend-helpers.js` — 459 passed, 0 failed (includes 6
`buildNodesQuery` + 5 `buildPacketsUrl` unit tests)
- [x] Navigate to `#/nodes?tab=repeater` — Repeaters tab active on load
- [x] Click a tab, verify URL updates to `#/nodes?tab=room`
- [x] Navigate to `#/packets?timeWindow=60` — time window dropdown shows
60 min
- [x] Change time window, verify URL updates
- [x] Navigate to `#/channels/{hash}` and click a sender name — URL
updates to `?node=Name`
- [x] Reload that URL — node panel re-opens
Closes #536
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
e046a6f632 |
fix: mobile accessibility — touch targets, ARIA, small viewport support (#630) (#633)
## Summary Fixes critical and major mobile accessibility items from #630, focused on small phone viewports (320px–375px). ### Critical fixes 1. **Touch targets ≥ 44px** — All interactive elements (filter buttons, tab buttons, search inputs, nav buttons, region pills, dropdowns) get `min-height: 44px; min-width: 44px` via `@media (pointer: coarse)` — desktop/mouse users are unaffected. 2. **ARIA live regions** — Added `aria-live="polite"` to: packet list (`#pktLeft`), node list (`#nodesLeft`), analytics content (`#analyticsContent`), live feed (`#liveFeed` with `role="log"`). Screen readers now announce dynamic content updates. 3. **Color-only status indicators** — Status dots in live view marked `aria-hidden="true"` (text labels like "Online"/"Degraded"/"Offline" already present alongside). 4. **Detail panel on mobile** — Side panel (`panel-right`) renders as a full-screen fixed overlay on ≤640px. Close button (✕) added to nodes detail panel. Escape key closes both nodes and packets detail panels. ### Major fixes 5. **Analytics tabs overflow** — Tabs switch to `flex-wrap: nowrap; overflow-x: auto` on ≤640px, preventing overflow on 320px screens. 6. **Table horizontal scroll** — Added `.table-scroll-wrap` class and `min-width: 480px` on `.data-table` at ≤640px for horizontal scrolling when columns don't fit. 7. **SPA focus management** — On every page navigation, focus moves to first heading (`h1`/`h2`/`h3`) or falls back to `#app`. Uses `requestAnimationFrame` for correct DOM timing. ### Bonus - Analytics tabs get `role="tablist"` + `aria-label` for screen reader semantics. ### Known follow-ups (not blocking) - Individual tab buttons should get `role="tab"` + `aria-selected` + `aria-controls` for complete ARIA tab pattern. - `sr-status-label` and `table-scroll-wrap` CSS classes are defined but not yet used in JS — ready for future use when status text labels and table wrappers are wired up. Closes #630 Co-authored-by: you <you@example.com> |
||
|
|
10f712f9d7 |
fix: restructure scroll containers for iOS status bar tap-to-scroll (#330) (#554)
## Summary Fixes #330 — iOS status bar tap-to-scroll broken because `#app` had `overflow: hidden`, preventing `<body>` from being the scroll container. ## Approach: Option B from the issue Instead of a JS polyfill, this restructures scroll containers so `<body>` is the primary scroll container by default, which iOS Safari requires for native status-bar tap-to-scroll. ### How it works **`#app` default (body-scroll mode):** Uses `min-height` instead of fixed `height`, no `overflow: hidden`. Content pushes beyond the viewport and body scrolls naturally. **`#app.app-fixed` (fixed-layout mode):** Restores the original `height: calc(100dvh - 52px); overflow: hidden` for pages that need constrained containers. The router in `app.js` toggles this class based on the current page. ### Fixed-layout pages (`.app-fixed`) These pages need fixed-height containers and are unchanged in behavior: - **packets** — virtual scroll requires fixed-height `.panel-left` to calculate visible rows - **nodes** — split-panel layout with independently scrollable panels - **map** — Leaflet requires fixed-dimension container - **live** — Leaflet map (also has its own `#app:has(.live-page)` override in live.css) - **channels** — split-panel chat layout - **audio-lab** — split-panel layout ### Body-scroll pages (no `.app-fixed`) These pages now let the body scroll, enabling iOS tap-to-scroll: - **analytics** — removed `overflow-y: auto; height: 100%` - **observers** — removed `overflow-y: auto; height: calc(100vh - 56px)` - **traces** — removed `overflow-y: auto; height: 100%` - **home** — removed `#app:has(.home-hero)` override (no longer needed) - **compare** — removed inline `overflow-y:auto; height:calc(100vh - 56px)` - **perf** — removed inline `height:100%; overflow-y:auto` - **observer-detail** — removed inline `overflow-y:auto; height:calc(100vh - 56px)` - **node-analytics** — removed inline `height:100%; overflow-y:auto` ### Files changed | File | Change | |------|--------| | `public/style.css` | `#app` default → `min-height`; added `.app-fixed` class | | `public/app.js` | Router toggles `.app-fixed` based on page | | `public/home.css` | Removed `#app:has()` workaround | | `public/compare.js` | Removed inline overflow/height | | `public/perf.js` | Removed inline overflow/height | | `public/observer-detail.js` | Removed inline overflow/height | | `public/node-analytics.js` | Removed inline overflow/height | ### What's preserved - Sticky nav (`position: sticky; top: 0`) — works with body scroll - Split-panel resize handles — unchanged, still in fixed containers - Virtual scroll on packets page — unchanged, `.panel-left` still has fixed height - Leaflet maps — unchanged, containers still have fixed dimensions - Mobile responsive overrides — unchanged Co-authored-by: you <you@example.com> |
||
|
|
64745f89b1 |
feat: customizer v2 — event-driven state management (#502) (#503)
## Summary Implements the customizer v2 per the [approved spec](docs/specs/customizer-rework.md), replacing the v1 customizer's scattered state management with a clean event-driven architecture. Resolves #502. ## What Changed ### New: `public/customize-v2.js` Complete rewrite of the customizer as a self-contained IIFE with: - **Single localStorage key** (`cs-theme-overrides`) replacing 7 scattered keys - **Three state layers:** server defaults (immutable) → user overrides (delta) → effective config (computed) - **Full data flow pipeline:** `write → read-back → merge → atomic SITE_CONFIG assign → apply CSS → dispatch theme-changed` - **Color picker optimistic CSS** (Decision #12): `input` events update CSS directly for responsiveness; `change` events trigger the full pipeline - **Override indicator dots** (●) on each field — click to reset individual values - **Section-level override count badges** on tabs - **Browser-local banner** in panel header: "These settings are saved in your browser only" - **Auto-save status indicator** in footer: "All changes saved" / "Saving..." / "⚠️ Storage full" - **Export/Import** with full shape validation (`validateShape()`) - **Presets** flow through the standard pipeline (`writeOverrides(presetData) → pipeline`) - **One-time migration** from 7 legacy localStorage keys (exact field mapping per spec) - **Validation** on all writes: color format, opacity range, timestamp enum values - **QuotaExceededError handling** with visible user warning ### Modified: `public/app.js` Replaced ~80 lines of inline theme application code with a 15-line `_customizerV2.init(cfg)` call. The customizer v2 handles all merging, CSS application, and global state updates. ### Modified: `public/index.html` Swapped `customize.js` → `customize-v2.js` script tag. ### Added: `docs/specs/customizer-rework.md` The full approved spec, included in the repo for reference. ## Migration On first page load: 1. Checks if `cs-theme-overrides` already exists → skip if yes 2. Reads all 7 legacy keys (`meshcore-user-theme`, `meshcore-timestamp-*`, `meshcore-heatmap-opacity`, `meshcore-live-heatmap-opacity`) 3. Maps them to the new delta format per the spec's field-by-field mapping 4. Writes to `cs-theme-overrides`, removes all legacy keys 5. Continues with normal init Users with existing customizations will see them preserved automatically. ## Dark/Light Mode - `theme` section stores light mode overrides, `themeDark` stores dark mode overrides - `meshcore-theme` localStorage key remains **separate** (view preference, not customization) - Switching modes re-runs the full pipeline with the correct section ## Testing - All existing tests pass (`test-packet-filter.js`, `test-aging.js`, `test-frontend-helpers.js`) - Old `customize.js` is NOT modified — left in place for reference but no longer loaded ## Not in Scope (per spec) - Undo/redo stack - Cross-tab synchronization - Server-side admin import endpoint - Map config / geo-filter overrides --------- Co-authored-by: you <you@example.com> |
||
|
|
f71e117cdd |
fix: reset restores home steps after SITE_CONFIG contamination (#460)
## 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) |
||
|
|
d1cb84b596 |
feat: Priority+ nav pattern for tablet viewports (768-1023px) (#345)
## Priority+ Navigation Pattern for Tablet Viewports Phase 2 of responsive nav improvements for #322. ### What this does On **tablet viewports (768-1023px)**, implements the [Priority+ navigation pattern](https://css-tricks.com/the-priority-plus-navigation-pattern/): - **5 high-priority tabs** shown inline: Home, Nodes, Packets, Map, Live - **6 low-priority tabs** collapse into a "More ▾" dropdown: Channels, Traces, Observers, Analytics, Perf, Lab - The "More" button highlights when a low-priority page is active **Desktop (>=1024px)** and **mobile (<768px)** behavior is unchanged. ### Changes | File | Change | |------|--------| | `public/index.html` | Added `data-priority="high"` to 5 primary nav links; added More button + dropdown menu | | `public/style.css` | Split ≤1023px hamburger query into tablet Priority+ (768-1023px) and mobile hamburger (<768px); added More dropdown styles | | `public/app.js` | Added `closeMoreMenu()`, More button toggle, outside-click/Escape close, active state on More button | | Cache busters | Bumped in same commit | ### Accessibility - `aria-haspopup="true"` and `aria-expanded` on More button - `role="menu"` / `role="menuitem"` on dropdown - Focus moves to first item on open - Escape key closes dropdown ### Testing - All 308 existing tests pass (217 frontend-helpers + 62 packet-filter + 29 aging) - No new dependencies added - No build step changes ### Breakpoint summary | Viewport | Behavior | |----------|----------| | >= 1024px | Full horizontal nav (unchanged) | | 768-1023px | Priority+ pattern: 5 tabs + More dropdown **← NEW** | | < 768px | Hamburger drawer with all items (unchanged) | --------- Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
2d8203ae17 |
fix: extend responsive nav hamburger breakpoint to 1024px (#343)
## Summary Extends the hamburger menu activation breakpoint from max-width: 640px to max-width: 1023px, making all 11 nav items accessible on tablets and small laptops where they were previously clipped/invisible. Fixes #322 ## Changes ### public/style.css - New @media (max-width: 1023px) block activates the hamburger menu and vertical drawer - Drawer has max-height: calc(100dvh - 52px) with overflow-y: auto for scrollability - z-index set to 1100 (consistent with nav layer) - ody.nav-open locks background scroll when drawer is open - Mobile-only rules (brand-text hidden, tighter nav-right gap) remain at 640px ### public/app.js - Extracted closeNav() helper for consistent drawer close behavior - Hamburger toggle now adds/removes ody.nav-open class - Drawer closes on: nav link click, Escape key, and route change (SPA navigation) ### public/index.html - Cache busters bumped for all CSS/JS assets ## What's NOT changed - Desktop layout (>=1024px) is completely untouched - No Priority+ pattern (Phase 2) - No map layout changes (Phase 3) - No new dependencies ## Testing - All 308 frontend tests pass ( est-frontend-helpers.js, est-packet-filter.js, est-aging.js) - Visual verification: hamburger activates at <=1023px, full bar at >=1024px --------- Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
ce6e8d5237 |
feat: show transport code (T_FLOOD) in packets view (#337)
## Summary Surfaces transport route types in the packets view by adding a **"T" badge** next to the payload type badge for packets with `TRANSPORT_FLOOD` (route type 0) or `TRANSPORT_DIRECT` (route type 3) routes. This helps mesh analysis — communities can quickly identify transported packets and gain insights into scope usage adoption. Closes #241 ## What Changed ### Frontend (`public/`) - **app.js**: Added `isTransportRoute(rt)` and `transportBadge(rt)` helper functions that render a `<span class="badge badge-transport">T</span>` badge with the full route type name as a tooltip - **packets.js**: Applied `transportBadge()` in all three packet row render paths: - Flat (ungrouped) packet rows - Grouped packet header rows - Grouped packet child rows - **style.css**: Added `.badge-transport` class with amber styling and CSS variable support (`--transport-badge-bg`, `--transport-badge-fg`) for theme customization ### Backend (`cmd/server/`) - **decoder_test.go**: Added 6 new tests covering: - `TestDecodeHeader_TransportFlood` — verifies route type 0 decodes as TRANSPORT_FLOOD - `TestDecodeHeader_TransportDirect` — verifies route type 3 decodes as TRANSPORT_DIRECT - `TestDecodeHeader_Flood` — verifies route type 1 (non-transport) decodes correctly - `TestIsTransportRoute` — verifies the helper identifies transport vs non-transport routes - `TestDecodePacket_TransportFloodHasCodes` — verifies transport codes are extracted from T_FLOOD packets - `TestDecodePacket_FloodHasNoCodes` — verifies FLOOD packets have no transport codes ## Visual In the packets table Type column, transport packets now show: ``` [Channel Msg] [T] ← transport packet [Channel Msg] ← normal flood packet ``` The "T" badge has an amber color scheme and shows the full route type name on hover. ## Tests - All Go tests pass (`cmd/server` and `cmd/ingestor`) - All frontend tests pass (`test-packet-filter.js`, `test-aging.js`, `test-frontend-helpers.js`) - Cache busters bumped in `index.html` --------- Co-authored-by: you <you@example.com> Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
8534cfdcc7 |
Fix reopened #284 customizer home regression (#317)
## Summary - prevent customizer panel open from auto-saving before initialization completes - stop `autoSave()` from mutating `window.SITE_CONFIG.home` - rehydrate `userTheme.home` from localStorage into `window.SITE_CONFIG` during app boot - add frontend regression tests for auto-save guard and home rehydration merge - bump `public/index.html` cache busters for updated frontend assets ## Validation - `npm run test:unit` Fixes #284 --------- Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
114b6eea1f |
Show build age next to commit hash in UI (#311)
## Summary - show relative build age next to the commit hash in the nav stats version badge (e.g. `abc1234 (3h ago)`) - use `stats.buildTime` from `/api/stats` and existing `timeAgo()` formatting in `public/app.js` - keep behavior unchanged when `buildTime` is missing/unknown ## What changed - updated `formatVersionBadge()` signature to accept `buildTime` - appended a `build-age` span after the commit link when `buildTime` is valid - passed `stats.buildTime` from `updateNavStats()` - updated frontend helper tests for the new function signature - added regression tests for build-age rendering/skip behavior - bumped cache busters in `public/index.html` ## API check - verified Go server already exposes `buildTime` on `/api/stats` and `/api/health` via `cmd/server/routes.go` - no backend API changes required ## Tests - `node test-frontend-helpers.js` - `node test-packet-filter.js` - `node test-aging.js` All passed locally. ## Browser validation - Not run in this environment (no browser session available). Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
928a3d995a |
feat(frontend): implement #286 P1 timestamp controls (#296)
## 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> |
||
|
|
6a3b8967b4 |
Frontend: timestamp display enhancement (issue #286) (#291)
## Frontend: Timestamp Display Enhancement Refs #286 — implements P0 frontend scope from the [final spec](https://github.com/Kpa-clawbot/CoreScope/issues/286#issuecomment-4158891089). ### What changed **Shared formatter (`public/app.js`)** - `formatTimestamp(isoString, mode)` — returns formatted string ("ago" or absolute) - `formatTimestampWithTooltip(isoString, mode)` — returns `{ text, tooltip, isFuture }` for dual-format hover - `timeAgo()` fixed: null → `"—"`, future timestamps shown with actual value (not clamped) **All surfaces updated** - `public/packets.js` — table rows + detail pane use shared formatter, hover shows opposite format - `public/live.js` — fixed inconsistency (`toLocaleTimeString` → shared formatter), same tooltip treatment - `public/nodes.js` — node timestamps use shared formatter **Future clock skew** - ⚠️ icon shown when timestamp is in the future, tooltip: "Timestamp is in the future — node clock may be skewed" **Customizer (`public/customize.js`)** - New "UI Settings" section with timestamp mode toggle (ago ↔ absolute) - Labeled as global setting - Persists to localStorage (`meshcore-timestamp-mode`), falls back to server default **CSS (`public/style.css`)** - `col-time`: min-width + nowrap for ISO timestamps - Mobile: shorter format (time only) instead of hiding column ### Testing - `node test-frontend-helpers.js` — formatter unit tests (null, ago, absolute, future skew) - `node test-packet-filter.js` — existing tests pass - `node test-aging.js` — existing tests pass Cache busters bumped in `public/index.html`. Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
71ec5e6fca |
rename: MeshCore Analyzer → CoreScope (frontend + .squad)
Phase 1 of the CoreScope rename — frontend display strings and squad agent metadata only. index.html: - <title>, og:title, twitter:title → CoreScope - Brand text span → CoreScope - og:image/twitter:image URLs → corescope repo (placeholder) - Cache busters bumped public/*.js headers (19 files): - All file header comments updated public/*.css headers: - style.css, home.css updated JavaScript strings: - app.js: GitHub URL → corescope - home.js: 3 fallback siteName references - customize.js: default siteName + heroTitle Tests: - test-e2e-playwright.js: title assertion → corescope - test-frontend-helpers.js: GitHub URL constant - benchmark.js: header string - test-all.sh: header string .squad: - team.md, casting/history.json - All 7 agent charters + 5 history files NOT renamed (intentional): - localStorage keys (meshcore-*) - CSS classes (.meshcore-marker) - Window globals (_meshcore*) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
3361643bc0 |
fix: #208 search results keyboard accessible — tabindex, role, arrow-key nav
- Search result items: tabindex='0', role='option', data-href (replaces inline onclick) - Delegated click handler via activateSearchItem() - Keydown handler: Enter/Space activates, ArrowDown/ArrowUp navigates items - ArrowDown from search input focuses first result - searchResults container: role='listbox' - Bump cache busters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
2d2e5625ce |
fix: resolve E2E test failures — engine badge, timing races, hidden pane
- app.js: render engine badge with .engine-badge span (was plain text) - test: fix #pktRight waitForSelector to use state:'attached' (hidden by detail-collapsed) - test: fix map heat persist race — wait for async init to restore checkbox state - test: fix live heat persist race — test via localStorage set+reload instead of click - test: fix live matrix toggle race — wait for Leaflet tiles before clicking - test: increase packet detail timeouts for remote server resilience - test: make close-button test self-contained (navigate if #pktRight missing) - bump cache busters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
b92e71fa0e |
refine version badge: clickable links, version only on prod
- Commit hash is now an <a> linking to GitHub commit (full hash in URL, 7-char display) - Version tag only shown on prod (port 80/443 or no port), linked to GitHub release - Staging (non-standard port) shows commit + engine only, no version noise - Detect prod vs staging via location.port - Updated tests: 16 cases covering prod/staging/links/edge cases - Bumped cache busters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
a7a280801a |
feat: display version and commit hash in stats bar
Add formatVersionBadge() that renders version, short commit hash, and engine as a single badge in the nav stats area. Format: v2.6.0 · abc1234 [go]. Skips commit when 'unknown' or missing. Truncates commit to 7 chars. Replaces the standalone engine badge call in updateNavStats(). 8 unit tests cover all edge cases (missing fields, v-prefix dedup, unknown commit, truncation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
e47b5f85ed |
feat: display backend engine badge in stats bar
Show [go] or [node] badge in the nav stats bar when /api/stats returns an engine field. Gracefully hidden when field is absent. - Add formatEngineBadge() to app.js (top-level, testable) - Add .engine-badge CSS class using CSS variables - Add 5 unit tests in test-frontend-helpers.js - Bump cache busters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
5b496a8235 |
feat: add missing payload types from firmware spec
Added GRP_DATA (0x06), MULTIPART (0x0A), CONTROL (0x0B), RAW_CUSTOM (0x0F) to decoder.js, app.js display names, and packet-filter.js. Source: firmware/src/Packet.h PAYLOAD_TYPE definitions. |
||
|
|
72ca713449 |
fix: branding from server config now actually works
Two bugs: 1. fetch was cached by browser — added cache: 'no-store' 2. navigate() ran before config fetch completed — moved routing into .finally() so SITE_CONFIG is populated before any page renders. Home page was reading SITE_CONFIG before fetch resolved, getting undefined, falling back to hardcoded defaults. |
||
|
|
8984b921f0 |
fix: server theme config actually applies on the client
- Dark mode: now merges theme + themeDark and applies correctly - Added missing CSS var mappings: navText, navTextMuted, background, sectionBg, font, mono - Fixed 'background' key mapping (was 'surface0', never matched) - Derived vars (content-bg, card-bg) set from server config - Type colors from server config now applied to TYPE_COLORS global - syncBadgeColors called after type color override |
||
|
|
0ac7f63035 |
Fix: localStorage preferences take priority over server config
app.js was fetching /api/config/theme and overwriting ROLE_COLORS, ROLE_STYLE, branding AFTER customize.js had already restored them from localStorage. Now skips server overrides for any section where user has local preferences. Also added branding restore from localStorage on DOMContentLoaded. |
||
|
|
041a249961 |
Fix: debounce theme-refresh 300ms — no more re-render spam
Color picker input events fire dozens of times per second while dragging. Now debounced to 300ms — page re-renders once after you stop dragging. |
||
|
|
0e59712a53 |
Fix: color changes re-render in-place without page flash
theme-changed now dispatches theme-refresh event instead of full navigate(). Map re-renders markers, packets re-renders table rows. No teardown/rebuild, no flash. |
||
|
|
c6801e4a9e |
Fix: node/type colors trigger page re-render, conflict badge uses status-yellow
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. |