mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 12:51:44 +00:00
d437958474df87556b429e6a2e691c7bca741512
90 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6dfe589b57 |
fix(#1668): per-route polish — hash cells, badges, /live, modals (M4) (#1681)
Partial fix for #1668 (M4 of 6). After M2 (color tokens, PR #1676, ~85% BLOCKER) and M3 (typography floor, PR #1679, ~87% MAJOR), what's left are route-specific structural issues that token/floor passes can't reach. M4 closes those with surgical carve-outs — no new top-level tokens, no semantic encoding flattened. ## Route × selector × fix | Route | Selector | Before | After | |---|---|---|---| | `/analytics?tab=hashsizes` `/analytics?tab=collisions` | `td.hash-cell` + `-collision/-taken/-possible` (302+ M1 violations) | 11px/400; collision-fg 3.61, taken-fg 2.5, possible-fg 1.9 on respective bg | 12px base, 12px/700 on semantic cells. Bg palette preserved (green/yellow/orange still distinct). Inline style in analytics.js bumped 11→12. | | `/packets` `/live` `/nodes` (everywhere `<span class="badge badge-*">`) | All 14 TYPE_COLORS badges (ADVERT, REQUEST, RESPONSE, …) | `${color}20` translucent wash with `color: ${color}` — ratio **1.0–4.25, all BLOCKER** | `syncBadgeColors` rewritten: pick readable fg by luminance, darken bg in 8% steps until AA (≥4.5:1). All 14 PASS (4.57–7.94). TYPE_COLORS itself unchanged — map dots / live-feed dots keep full hue. | | `/live` | `.vcr-live-btn` ("LIVE") | `rgba(239,68,68,0.2)` + status-red fg = **1.0:1** | Solid `--status-red` + #fff = 5.25:1; 12px/700 | | `/live` | `.vcr-scope-btn.active` (1h/6h/12h/24h selected) | `--accent-bg` wash + `--text` = 2.98:1 BLOCKER | `--accent-strong` + `--text-on-accent` (M2 tokens, AA) | | `/live` | `.vcr-btn` `.vcr-scope-btn` | 0.9rem/400, 0.75rem/400 (thin-small) | 14px/500, 12px/500 desktop; 12px/600 ≤640px | | `/live` | `.live-feed-empty` | 12px/400 (thin-small) | 12px/500 | | `/packets` (path hops) | `.path-hops .hop-named` | font-size inherited (variable) | explicit 12px/600 | ## TDD & gating - **RED** `341f47f1` — 23 assertion failures (9 typography + 14 badge-contrast). New gate `test-issue-1668-m4-per-route.js` executes `syncBadgeColors` in a VM sandbox and asserts each emitted `.badge-*` rule clears WCAG AA; also checks rule-level font-size/font-weight floors. - **GREEN** `6ef17491` — both axes 0/0. - Test wired into `.github/workflows/deploy.yml:144` alongside M3. - Anti-tautology proven locally: `git stash public/roles.js` returns the test to FAIL with the badge assertions; pop restores GREEN. ## Re-scan findings `a11y-audit/m4-rescan.jsonl` — `/live` (timed out in M1) now probes cleanly: 29 dark / 39 light residuals all caught by this PR. Channel-add and customize modals probed clean (M2 tokens already cover; nothing chip-level needed). ## Out of scope M5 (axe CI gate) and M6 (letsmesh side-by-side A/B) are next milestones. --------- Co-authored-by: agent <agent@openclaw.local> Co-authored-by: meshcore-bot <bot@meshcore> Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
167af54eb8 |
polish(#1645): tighten observer compare — checkboxes, hierarchy, selector strip (#1647)
## Summary Polish follow-ups for the #1644/#1645 observer-comparison redesign — addresses all 5 parent visual-review findings + 3 Tufte additions in one PR. Fixes #1646. ## Coalesced fix list (status: ✅ all landed) | # | Tag | Item | Fix | Evidence | |---|---|---|---|---| | 1 | [both] | Native checkboxes were bare white squares against dark theme | Global `input[type=checkbox]/[type=radio] { accent-color: var(--accent) }` + `color-scheme: dark` on dark theme blocks. UA renders themed checkboxes everywhere now. | `screenshots-1646/after-observers-selected-dark.jpg` vs `before-observers-selected-dark.jpg` | | 2 | [parent] | Compare CTA was heavy-blue primary, redundant once both dropdowns set | `#compareBtn` now `.btn-ghost`; hidden when both observers selected (collapsed state) | `after-compare-desktop-dark.jpg` — no blue button visible | | 3 | [parent] | "vs" label at parity with dropdowns | 10px, centered, letter-spaced, opacity 0.7 | Compare-page screenshots — "vs" sits as small-caps annotation | | 4 | [parent] | SHARED column had three competing font weights; count outshone the percentage | Inverted hierarchy via new `.compare-strip-mid-pct` + `.compare-strip-mid-pct-unit`. 87% leads at `var(--fs-xl)` accent; "3,452 shared" demotes to `var(--fs-sm)`; "OF ALL UNIQUE" stays 10px caps | `after-compare-desktop-dark.jpg` middle column | | 5 | [parent] | Selector strip competed with headline strip for "look here first" attention | `.compare-controls.is-collapsed` (toggled when both observers selected) shrinks padding, hides labels + Compare button, narrows dropdowns. A/B swap still reachable | `after-compare-desktop-light.jpg` — picker compressed above the headline | | 6 | [tufte] | Decorative left accent border on `.compare-asym-line` encoded nothing | Removed (chartjunk) | `after-compare-desktop-dark.jpg` — squared cards | | 7 | [tufte] | Decorative left green border on `.compare-type-summary` encoded nothing | Removed (chartjunk) | Same | | 8 | [tufte] | Bare "87" was ambiguous; needed unit integrated as annotation | Wrapped `%` in smaller `.compare-strip-mid-pct-unit` — words and graphics co-located | Middle column hierarchy | ## Push-backs / scope discipline - **Did NOT remove the selector strip entirely.** Parent rule + operator UX: A/B swap must remain reachable. Collapsing > removing. - **Did NOT introduce a custom checkbox widget.** UA native + `accent-color` + `color-scheme` is the minimal-ink fix; new SVG/library would add chrome the data didn't ask for. - **Did NOT add new color tokens.** All restyling uses existing `--accent`, `--text`, `--text-muted`, `--surface-1/2`, `--border`. ## TDD - Red commit: `2863cfb3 test(#1646): RED — assertions for compare-polish ...` — `test-issue-1646-compare-polish.js` 9 assertions, all FAIL on master. - Green commits (3 logical groups): 1. `deb0737f fix(#1646): theme native checkboxes — global accent-color + color-scheme on dark` 2. `fb791a6f fix(#1646): tighten compare-strip hierarchy + scrub decorative borders` 3. `8033ac36 fix(#1646): ghost-style Compare CTA + collapse the picker once both observers chosen` Final: `node test-issue-1646-compare-polish.js` → 9/9 pass; `node test-issue-1644-redesign.js` → 13/13 pass (no regression); `node test-compare-overlap.js` → 6/6 pass; `node test-frontend-helpers.js` → 611/611 pass. ## Visual verification All staging-validated via local headless chromium against the hot-swapped files at `http://20.x.y.z` (staging). Surface matrix covered: - Observers list — desktop dark (with 2 rows checked) — themed accent on checkboxes - Observers list — mobile 375px dark - Compare page — desktop light + dark - Compare page — mobile 375px dark **Reviewer note: screenshot artifacts were captured locally (sandbox does not have a GitHub UI session for attachment upload).** Paths below — pull these from the same workspace location if you want to inspect: ``` screenshots-1646/before-observers-desktop-dark.jpg ← bare white checkboxes screenshots-1646/before-observers-selected-dark.jpg ← bare white checked + unchecked screenshots-1646/before-compare-desktop-dark.jpg ← blue Compare CTA; flat hierarchy; deco borders screenshots-1646/after-observers-selected-dark.jpg ← themed checkboxes screenshots-1646/after-observers-mobile-dark.jpg screenshots-1646/after-compare-desktop-light.jpg ← collapsed picker; pct leads mid column screenshots-1646/after-compare-desktop-dark.jpg screenshots-1646/after-compare-mobile-dark.jpg ``` No raw `MEDIA:` UUIDs in this body — that was the mistake on #1645 and is not being repeated. If maintainers want the images inline, drag-drop the JPGs into a follow-up comment via the GitHub web UI. ## Risk Low. Pure CSS + one class-toggle in `compare.js`'s `updateBtn` (idempotent, no race, no event loop change). `accent-color` is supported in all evergreen browsers since 2021; degrades gracefully (UA white fallback) on the rare browser that ignores it — i.e. exactly the current-master state. --------- Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
c93ae67ed0 |
redesign(#1644): make observer comparison feel amazing — themed button vocabulary + state-preserving multi-select + Tufte-grade compare page (#1645)
## What was wrong PR #1642 promoted observer comparison to a first-class IA citizen but shipped three problems: `class="btn-secondary"` buttons that fell back to browser-default white/gray because no such CSS rule existed; the 30-second auto-refresh blew away `<tbody>.innerHTML` and destroyed every compare-select checkbox along with its state; and the `#/compare` page itself showed three card-boxes forcing the eye to do mental subtraction. ## Design rationale (Tufte) The comparison page now leads with **one row of three numbers above one proportional diff bar** — shared-axis small multiples in place of three nearly-identical cards. The eye reads the whole comparison in one fixation. Asymmetric reach is demoted from two big cards to two compact, ctx-style sentences with mono-numeric percentages. The button vocabulary borrows route-view v2's restraint: surface tokens for neutral chrome, accent only on the primary CTA, no gradients or shadows. The checkbox column visually recedes when no row is picked (empty-state IS the design) and lights up only once a selection exists. Everything composes existing CSS tokens — no new top-level color literals — so all themes (light, dark, CB presets) Just Work. ## Inventory of CSS additions | Selector | Role | |---|---| | `.btn-secondary`, `.btn-secondary[disabled]` | Themed neutral button (low-emphasis CTA) | | `.btn-ghost` | Minimal transparent-until-hover variant (reserved for future) | | `.compare-page`, `.compare-page .page-header` | Page-level container, overrides `.page-header { justify-content: space-between }` | | `.compare-breadcrumbs` | Themed breadcrumb link strip | | `.compare-controls`, `.compare-selector`, `.compare-select-group`, `.compare-select`, `.compare-vs`, `.compare-btn` | Selector strip — re-themed with surface tokens | | `.compare-strip`, `.compare-strip-row`, `.compare-strip-side`, `.compare-strip-mid`, `.compare-strip-name`, `.compare-strip-count`, `.compare-strip-mid-count`, `.compare-strip-mid-label`, `.compare-strip-sub` | The headline small-multiples row (A \| shared \| B) | | `.compare-bar`, `.compare-bar-seg`, `.compare-bar-{a,both,b}`, `.compare-bar-legend`, `.compare-legend-item`, `.compare-dot-{a,both,b}` | Single proportional diff bar | | `.compare-asym`, `.compare-asym-line`, `.compare-asym-pct` | Compact directional-reach sentences (replaced the two big cards) | | `.compare-type-summary`, `.compare-type-summary-label`, `.compare-type-badge` | Shared-type pill row with ctx-style border-left accent | | `.compare-tabs`, `.compare-tabs .tab-btn`, `.tab-btn.active` | Tabs reskinned to match the muted-then-accent pattern | | `.compare-summary-text`, `.compare-warning`, `.compare-good` | Themed status notes | | `.col-compare-select`, `.col-compare-select input[type="checkbox"]` | Compare-select column — muted when empty, full text + `--selected-bg` row tint when populated | | `.obs-table.has-compare-selection` | Marker class so the column changes intensity only when something is picked | | `.observers-page .page-header`, `.obs-refresh-spacer` | Header layout (flex with right-side refresh icon) | | `.observer-detail-page .compare-with-group` | Grouped picker + Compare button surface on the detail page | **Tokens used:** `--surface-1`, `--surface-2`, `--border`, `--accent`, `--accent-hover`, `--text`, `--text-muted`, `--row-hover`, `--hover-bg`, `--selected-bg`, `--status-green`, `--status-amber`, `--status-amber-light`, `--status-amber-text`, `--radius-sm`, `--radius-md`, `--badge-radius`, `--space-xs..xl`, `--fs-sm..xl`, `--mono`. **No new top-level color tokens were introduced.** ## Before PR #1642's bare `<button class="btn-secondary">` rendered with the browser-default white pill and the compare page showed three rgba-tinted cards (`rgba(34,197,94,0.1)`, `rgba(74,158,255,0.1)`, `rgba(255,107,107,0.1)`) — chartjunk with no theme awareness. See #1644 description for the bug repro. ## After (screenshots) **Desktop — observers page (light, empty + selected states):** - Empty: `MEDIA: 42d90aa5-643c-4e88-8b5d-3383cfa2dfe4.jpg` - Two selected (rows tinted, button enabled): `MEDIA: a6d9b397-ffe5-4eeb-b07b-ef89041ab6ea.jpg` **Desktop — observer detail (light, picker + Compare grouped):** `MEDIA: 17b9b47d-5e97-4293-8558-e9b37c244335.jpg` **Desktop — compare page (light, real data via mock — fixture has 0 overlap):** `MEDIA: be169bf2-f31b-480a-97b1-4f678745471b.jpg` **Desktop — compare page (dark):** `MEDIA: 436477a7-600c-4ac4-aa9d-97db968246d3.jpg` **Desktop — observers (dark, two selected):** `MEDIA: 850242c3-db77-460f-895f-0a6e6b150758.jpg` **Mobile 375px — observers (dark):** `MEDIA: 338b543c-0705-41ec-95da-e2c2a8db2065.jpg` **Mobile 375px — compare page (dark, stacks cleanly):** `MEDIA: 380a984c-26f0-4f47-b4ba-d655571721c9.jpg` ## Test plan - `node test-issue-1644-redesign.js` — 8/8 (new behavioral suite for this PR) - `node test-issue-1562-observers-summary.js` — 13/13 - `node test-compare-overlap.js` — 6/6 - `node test-compare-flood-filter.js` — 6/6 - `node test-frontend-helpers.js` — 611/611 - `node scripts/check-css-vars.js` — 0 undefined refs across 1901 var() calls - Browser-validated against local fixture build at `localhost:13580`: desktop light/dark, mobile 375px light/dark, observers + detail + compare pages. Checkbox preservation verified by manual refresh click — state survives the tbody rewrite. ## TDD - Red commit: `94e019c5` — 7 behavioral assertions that all FAIL on master (no top-level `.btn-secondary`, no `preserveCompareSelection` helper, rgba literals in compare-card rules). - Green commit: `a246208d` — implementation. All 8 assertions pass (the rgba assertion was relaxed to a conditional check after the cards were removed entirely in favor of the strip; an additional `.compare-strip exists` assertion was added). ## Out of scope - The server-side `&since=...` parser is strict about RFC3339 and rejects the `.000Z` suffix the frontend emits; this means the comparison page shows zeros against any data > 24h old. Filed separately — not a regression introduced by this PR. Screenshots showing populated numbers use a `comparePacketSets` test stub. - Backend Go untouched. Fixes #1644 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
16c7ea4b82 |
fix(#1528): theme-track .vcr-scope-btn.active + .copy-link-btn:hover backgrounds (#1578)
Red commit:
|
||
|
|
373ee81641 |
fix(UI): Additional fixes for issue #1532 (#1580)
- Eliminated extra space to the right of the map filters. - Made the map filters and mesh live a single line with a divider - Resized the input and dropdowns in the map filters so they meet WCAG 2.5.5 by being at least 44px high, but appearing 30px high - Turned the filters cog and the fullscreen button into native leaflet icons that are large enough to meet WCAV 2.5.5 compliance - Increased the size of the zoom buttons to meet WCAG 2.5.5 compliance on both the live and map pages - If the top nav bar is pinned, it won't disappear during fullscreen but if it isn't pinned, it will disappear with everything else. - The cog and full screen button change color to show they're active Final Outcome in 4k <img width="2878" height="1406" alt="image" src="https://github.com/user-attachments/assets/28db46a2-f1bb-4d9c-9d77-30c444b4ef3d" /> Final Outcome in 1080p <img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/120be8ec-0279-40fc-925a-243e9c0bcc1c" /> |
||
|
|
d7bd9d57b8 |
feat(live): fullscreen toggle + collapse controls by default (closes #1532) (#1572)
Closes #1532.
## What
Implements the triage's 3-step fix path + tufte keyboard shortcut:
1. **`.live-controls` collapsed by default at all viewports** (was
≤768px only). The existing ⚙ pin reveals the toggles row on demand —
parity with the map-controls accordion pattern in `map.js`.
2. **New `#liveFullscreenToggle` button (⛶) next to ⚙.** Click or press
`F` to flip `body.live-fullscreen`. CSS under that class hides:
- `.live-header-body` (title)
- `.live-controls-body` (toggle row contents)
- `.vcr-controls` and `.vcr-bar` (timeline scrubber)
- `.bottom-nav`
- secondary panels (`.live-feed`, `.live-legend`, related show-buttons)
3. **`.live-stats-row` stays pinned top-right** with translucent chip
styling so the 3 KPI pills (nodes / active / pkts·min) earn permanent
residence per the tufte finding.
## Tufte rationale (from triage)
> data-ink ratio is poor — 11 controls + 3 KPIs displayed permanently
steal pixels from THE data (the firework animation). Defaults-on chrome
should collapse behind a pin/cog; only the 3 stat pills earn permanent
residence (sparkline-grade density). … "Fullscreen" is the right
primitive — Tufte's "shrink principle" says strip until unreadable, then
add back.
## Keyboard shortcut
`F` toggles fullscreen. Guards:
- Skips when focus is in `INPUT`/`TEXTAREA`/`SELECT`/contenteditable (no
interference with node-filter / audio sliders typing).
- Skips when modifier keys are held.
- Only fires on the `.live-page` route.
- State persists across reloads via `localStorage('live-fullscreen')`.
## TDD
| Commit | SHA | What |
|--------|-----|------|
| RED | `852a474b` | Source-invariant assertion test
`test-issue-1532-live-fullscreen.js` (17 assertions, all fail against
master). |
| GREEN | `906c6cc0` | Implementation: HTML button, JS click+keydown
wiring, CSS body-class rules + top-level `.is-collapsed` rule. |
Verify the RED commit gates the change:
```
git checkout
|
||
|
|
99cea7bf72 |
fix(ui): Fix area not under cog and let live filters break out of scrolling container and improve metrics layout. Should resolve Issue #1529 (#1531)
### Description This PR addresses several visual and UX issues on the Live page, specifically focusing on mobile viewport constraints and filter accessibility. **Changes:** 1. **Dropdown Clipping Fix**: Previously, the Node, Region, and Area filters were nested inside `.live-toggles`. On narrow screens, `.live-toggles` becomes a horizontally scrolling container (`overflow-x: auto`), which unintentionally clipped the absolute-positioned dropdown menus for these filters. They have been moved to `.live-controls-body` as siblings, allowing their dropdowns to correctly break out and overlay the map. 2. **Cog Positioning**: The settings cog (`#liveControlsToggle`) has been pushed to the far right of the metrics header using `margin-left: auto`, creating a cleaner visual separation. 3. **Filter Spacing**: When the controls panel is expanded, a `12px` top margin is now applied to push the filter buttons further away from the metrics row for better touch targets and readability. 4. **Test Updates**: The E2E Playwright test for the Area dropdown was updated to click the cog menu first, matching the new DOM structure. 5. **Area outside cog**: Resolves the initial issue of the area dropdown being outside of the cog on a mobile display ### Performance Justification This is a pure HTML/CSS structural refactor. There are no additional per-item calculations or API calls introduced. Moving the DOM nodes out of the scrolling container has zero impact on render loop complexity, and no new JavaScript event listeners were added to the hot path. ### Testing - [x] Unit tests pass (`npm test`) - [x] Playwright E2E tests pass (updated to reflect the cog interaction) - [x] Verified visually in browser (Desktop and Mobile viewports) |
||
|
|
2e70bcb671 |
UI accent partial fix for issue #1528 (#1530)
Made the suggested changes as listed in the fix path provided by @Kpa-clawbot Fix path: `style.css:1244` `.field-table .section-row td` → `color: var(--text)` (or new `--section-header-fg`). `style.css:2620-2631` `.copy-link-btn` → `color: var(--text);` background/border via `--accent-bg` / `--accent-border` tokens with safe defaults. `live.css:987` `.vcr-scope-btn.active` → same token swap; ensure text remains `--text` on the tinted bg. `nodes.js:212` `.multibyte-badge` → move inline styles to style.css, `color:var(--text)`, keep `--accent-bg` background. When creating the defaults for `--accent-bg` and `--accent-border`, I chose to go with the default style values embedded in nodes.js, as that was the safest bet. We should probably extend the custom themes to include these variables as well as not to confuse users if they see it. This also causes the delima of, sometimes the `--accent` is use as the background for objects, and not `--accent-bg`, example: `btn active` has background set to` --accent` and border set to `--accent`. If we don't extend the config to accept accent-bg and accent-border, we risk users still making accents of light blue that will be drown out with the defaults we've set. Also updated the badge above the multi-byte badge that contains X bytes of the nodes public key, where X is determined by the path byte length. This was done because it had styles set that were easy to add to the styles.css file, to clean up coe. The node-type badge above it is unfortunately driven by javascript in the nodes.js page, and requires syling. **Note:** Accidentally added ghost changes into this push for a second time. They can be ignored as they were previously merged and shouldn't have been seen as new. |
||
|
|
0273f1546e |
fix(live/ui): Fixed a nav-right pin bug (#1526)
## Summary
Fixes a visual bug on the Live page where the navigation bar layout
would break, causing the right-side icons (search, theme toggle,
hamburger menu) to be pushed into the middle of the screen.
## Cause
The Live page dynamically injects a "📌" button to let users lock the
auto-hiding header. However, `live.js` was appending this button as a
direct child of the outer `.nav-bar` container.
Because `.nav-bar` uses flexbox with `justify-content: space-between` to
separate the left, center, and right sections, adding a 4th top-level
child threw off the distribution of space, squeezing `.nav-right` toward
the center.
## Changes
- **DOM Placement (`live.js`)**: Modified the injection logic to target
`.nav-right` and use `appendChild()` so the pin button is cleanly nested
at the far right of the existing right-side cluster (past the hamburger
menu).
- **CSS Cleanup (`live.css`)**: Removed `margin-left: auto;` from
`.nav-pin-btn` as it is no longer necessary and could cause spacing
issues inside the `.nav-right` flex container.
## Verification
- Verified the pin button renders seamlessly on the far right of the
Live page.
- Confirmed the outer `.nav-bar` layout strictly maintains its
left/center/right alignment.
- Confirmed there are no test regressions (the E2E test
`test-issue-1510-live-nav-pin-e2e.js` selects by ID and continues to
pass flawlessly).
|
||
|
|
914f869421 |
Make packet movement on the live map hardware-accelerated using HTML5 (#1490)
Instead of forcing Leaflet to recalculate and paint heavy SVG DOM nodes 60 times a second for every moving packet, we will draw the flying dots and lines directly onto a hardware-accelerated HTML5 <canvas> overlaid on the map. Once the animation finishes, it will drop a static Leaflet line to handle the fading tail effect. --------- Co-authored-by: KpaBap <kpabap@gmail.com> |
||
|
|
788a509e73 |
refactor: move version/commit badge from navbar to Perf dashboard (#1503)
## Summary The version/commit badge currently rendered in the nav stats bar (alongside packet counts, node counts, and observer counts) is operator-facing diagnostic information — not something end users need visible on every page load. For most visitors, it adds visual noise without adding value. ## Changes - **perf.js**: Add a **Version** card to the Perf dashboard overview row. Shows `version` + short `commit` hash, both already available from `/api/health` (no new API surface needed). Card renders conditionally — if neither field is set it stays hidden. - **app.js**: Remove `formatVersionBadge()` and `formatEngineBadge()` helper functions (now unused); strip the badge call from `updateNavStats()` so the navbar shows only packet/node/observer counts. - **style.css**: Remove now-dead `.nav-stats .version-badge`, `.nav-stats .engine-badge`, and their link sub-rules. ## Rationale The Perf page is explicitly the right place for this information — it's already scoped to operators and developers who want to know what version is running. The navbar is a high-visibility surface shared by all users; version strings belong in a diagnostic context, not a navigation bar. Net result: navbar is cleaner for end users; operators can still find version info immediately on the Perf tab. |
||
|
|
e11ce54059 |
fix(#1480): update E2E #534 to click navbar mirror; simplify CSS (#1484)
Sequence of errors: - #1475: hid in-page button with visibility:hidden \u2192 Playwright won't click visibility:hidden \u2192 broke E2E #534 - #1482: tried opacity:0 instead \u2192 Playwright won't click opacity:0 either \u2192 still broken - This PR: UPDATE THE TEST instead of fighting Playwright. The mobile UX since #1471 is: operator-visible Filters control = navbar mirror (.filter-toggle-btn-mirror). The test should click THAT, not the now-hidden in-page button. Test now tries the mirror first, falls back to in-page button for any test rig without the mirror script. CSS simplified to display:none. Unblocks #1480 (#1478 naive-TS observer UI surface) CI. Also any other PR inheriting this same regression. Hot-deploy candidate (CSS + test only). Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
d964c27964 |
feat(mobile): packets UX overhaul + nav surface + map inset + channel synthesis fixes (#1471)
## Summary Mobile UX overhaul for the packets surface plus two discoverable defects found along the way. All UI changes are mobile-only (`@media (max-width: 900px)` or `isMobile()` gates) — desktop unchanged. ## Closes - #1415 — packets layout cross-viewport jank - #1458 — Tufte mobile packets critique (P0s) - #1461 — Tufte v2 mobile packets critique (P0/P1) - #1467 — Favorites/Search/Customize unreachable on mobile - #1468 — client-side "unknown" channel synthesis - #1470 — node-detail map inset doesn't honor customizer dark provider ## Commits 1. `fix(#1468): drop client-side "unknown" channel synthesis` — `channels.js` 2. `feat(#1470): node-detail map inset honors customizer dark-tile provider` — `nodes.js`, `roles.js` 3. `feat(mobile): packets UX overhaul + bottom-nav More controls (#1415, #1458, #1461, #1467)` — `style.css`, `index.html`, `mobile-page-actions.js` (new) ## Mobile-list view changes - Kill empty chevron rail - Slim sticky THEAD (24px, retains sort affordance per operator preference) - Hide entire page-header on mobile - Mirror pause + Filters pill into navbar via new `mobile-page-actions.js` - Convert group-header `toggle-select` → `select-hash` on mobile (no dead-end expand) ## Mobile detail-panel changes - Drop redundant src→dst line (identity already in sticky header) - Hide boxed "decoded message" duplication card - Hide PAYLOAD TYPE row (already in header badge) - 2-col label/value grid (cuts panel height ~40%) - Sticky in-sheet header for packet identity - Kill iOS-style drag handle (conflicts with browser pull-to-refresh) - Make ✕ close visible + always reachable - Outer sheet `overflow:hidden`, inner content `overflow-y:auto` (scrollable region distinct, scrollbar visible) - Bottom-nav clearance (`padding-bottom: 60px`) - Close detail sheet on route change away from /packets - Tap-to-toast popovers for score tooltips (`title=` doesn't fire on touch) ## Mobile nav surface - Mirror Favorites ⭐ / Search 🔍 / Customize 🎨 into bottom-nav More sheet (#1467) - Brand stays in top nav; per-page controls (pause, Filters) injected into `.nav-left` ## Other fixes shipped together - **#1468**: drop CHAN messages with no decoded channel name (eliminates fake "unknown" channel row) - **#1470**: `_applyTilesToNodeMap` helper — node-detail inset map reads from `MC_TILE_PROVIDERS[active]` instead of hardcoded OSM; honors customizer's dark-tile provider pick + applies invert filter for inverted variants - `getTileUrl()` + new `getActiveTileProvider()` in `roles.js` now consult `MC_TILE_PROVIDERS` ## CDP verification (local chromium) Tested on staging at viewport 390×844 + 1206×928. | Surface | Before | After | |---|---|---| | Chrome above first data row | 231px (27% viewport) | ~80px (10% viewport) | | Packets visible above fold | 10 | 17 | | Detail panel duplications | 3× identity | 1× (header only) | | Mobile group-expand UX | dead-end (no chevron) | converts to select-hash | | Score tooltips on touch | broken (title= silent) | tap → toast popover | | Node detail map inset (dark mode) | always OSM light tiles | honors customizer provider + invert filter | | Bottom-nav More controls | Dark mode only | + Favorites, Search, Customize | ## What's NOT in this PR - Paths-through-node sort fix lives in #1431 (parallel PR for #1145) - Detail-panel hex byte-grid behind disclosure — operator wants it; follow-up - Group-header row sizing (some render 200–700px tall) — existing behavior, follow-up ## Test plan - [ ] Existing frontend tests stay green (`test-issue-1415-packets-layout.js`, `test-issue-1420-tile-providers.js`, `test-issue-1454-channels-toggle.js` all pass locally on this branch) - [ ] Existing Playwright E2E stays green - [ ] CDP on local chromium: 390×844 mobile + 1024×768 tablet + 1440×900 desktop — no regressions --------- 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> |
||
|
|
5cc7332583 |
feat(live): clickable path overlay — packet info popup (closes #771 M2) (#923)
After a path animation completes, keeps an invisible clickable polyline on the map for 30s. Clicking it shows a compact Leaflet popup with type badge, hop chain, relative time, and a link to the full packets page. Popup auto-dismisses after 20s. ## Changes - `clickablePathsLayer`: new Leaflet layer for invisible hit-target polylines - `buildClickablePathPopupHtml()`: pure function generating popup HTML (type badge, hop chain, time, hash link) - `pruneClickablePaths()`: TTL (30s) + FIFO eviction (max 50); runs on existing `_pruneInterval` - `registerClickablePath()`: adds invisible polyline with click → popup handler - `animatePath()`: accepts optional `pktMeta` (`hash`, `ts`); calls `registerClickablePath` on completion - Teardown clears `clickablePathsLayer` and `clickablePaths` ## Tests 7 new unit tests; 77 pass, 0 regressions. Closes #771 (M2 of 3) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
46ce9590f1 |
fix(#1270): Prefix Tool Network Overview shows configured-hash-size counts, not math-only slices (#1271)
Red commit: `6b68080c24106301b6bfc25f8a05484f07d0612d` (test added that fails on master). CI: see Checks tab on this PR. Fixes #1270. ## Problem Two analytics surfaces told contradictory stories about prefix usage: - **Prefix Tool → Network Overview** showed e.g. `168 / 65,536` for the 2-byte tier — a pure math fact: every repeater pubkey sliced to 2 bytes yields N distinct values. Because collisions are rare, this number always equals (or nearly equals) the repeater count, making it look like the whole network uses 2-byte hashing. - **Hash Stats → By Repeaters** showed configured-hash-size counts straight from `/api/analytics/hash-sizes` `distributionByRepeaters` — usually a minority on 2-byte and near-zero on 3-byte. The Prefix Tool was presenting a math fact as if it were operational truth. ## Fix `renderPrefixTool` now also fetches `/api/analytics/hash-sizes` and restructures each tier card into three labeled stats with explicit hierarchy: 1. **Primary** — `X of Y repeaters configured` (from `distributionByRepeaters`). Same source the Hash Stats tab uses, so the two pages agree exactly. 2. **Operational collisions** — colliding slices among repeaters configured for *this* hash size only (matches Hash Issues semantics). 3. **Theoretical** (secondary, smaller, dashed-rule footnote) — `X unique N-byte slices across all repeater pubkeys (of Y possible)`. The math fact is preserved as educational info, no longer impersonating operational truth. The "Total repeaters" card now also notes how many have a known configured hash size. The "About these numbers" footer was rewritten to explain the three numbers and link to both Hash Stats and Hash Issues. The prefix collision detector (Check / Generate panels) is unchanged — it still scans every repeater pubkey because that is its job. ## Test Added `#1270 Prefix Tool primary counts match Hash Stats By Repeaters` to `test-e2e-playwright.js`. It fetches `/api/analytics/hash-sizes` for the ground-truth `distributionByRepeaters`, then visits `#/analytics?tab=prefix-tool`, opens Network Overview, and scrapes the primary count via a new `data-pt-configured="<bytes>"` `data-value="<count>"` marker on each tier card, asserting exact equality for 1/2/3-byte. - Red commit `6b68080c` (test only): fails on master with `NO data-pt-configured marker`. - Green commit `12ed2789` (fix): test passes; full E2E suite `123/126 passed, 3 skipped`. ## Acceptance - [x] Prefix Tool Network Overview shows configured-hash-size repeater counts as the primary number - [x] "Unique slices" math is shown as secondary/educational - [x] Two pages tell the same story (E2E asserts byte-equal match) - [x] E2E asserts the configured-count matches what Hash-Sizes tab shows at the same point in time |
||
|
|
74685ac82f |
fix(#1243): node detail mobile QR overlays map semi-transparently (#1245)
RED commit `fc9b619a` — CI: https://github.com/Kpa-clawbot/CoreScope/actions Fixes #1243. ## Problem On `#/nodes/<pubkey>` at 375×800, the QR code rendered as a separate ~250px-tall panel below the map. Desktop already overlays the QR semi-transparently via `.node-map-qr-overlay` for the compact view. ## Fix Extend the mobile breakpoint (`@media (max-width: 640px)`) so the full-screen `.node-top-row` mirrors the desktop overlay pattern: - `.node-top-row` → `position: relative`; map wrap expands to 100% - `.node-qr-wrap` → `position: absolute; bottom/right: 8px; z-index: 400` - Semi-transparent background (`rgba(255,255,255,0.85)` light / `0.4` dark) - Caption hidden in overlay (already shown above) Desktop (≥768px) flex layout untouched. ## TDD - RED `fc9b619a` — E2E at 375×800 asserts QR is `position: absolute|fixed`, overlaps map rect, and bg alpha < 1. - GREEN `ded978c0` — CSS adds overlay rule. ## Verification Preflight clean. Desktop layout unaffected — change is scoped inside `@media (max-width: 640px)`. ## Files - `public/style.css` (+29) - `test-e2e-playwright.js` (+57) --------- Co-authored-by: clawbot <clawbot@local> |
||
|
|
30ff45ad34 |
fix(#1220): collapse MESH LIVE mobile header into a single ~50px strip (#1223)
RED commit `c1a8cea` — E2E at 375x800 asserts MESH LIVE header is either ≤60px (collapsed) or ≥60px with a visible body. Fails on master with `height=118, bodyVisible=false, ctrlsVisible=false` — the empty-chrome middle state. CI for red commit: https://github.com/Kpa-clawbot/CoreScope/actions (will populate after push). ## Diagnosis On `(max-width: 768px)`, `#1180` collapses both `.live-header-body` and `.live-controls-body` to `display:none`. But `.live-controls` carries `flex: 0 0 100%` from the wide-viewport rule (introduced for `#1219` so the toggles wrap onto their own row below the title on tablet). On mobile, with the body hidden, that 100% basis still forces the gear button onto a full-width second row inside `#liveHeader`'s flex-wrap, ~60px tall — yielding the `~118-200px` empty panel the bug screenshot shows (the count badge + 📊 toggle on row 1, gear alone on row 2, nothing else). ## Fix — Option C Inside `@media (max-width: 768px)`, when `.live-controls.is-collapsed`: - drop `flex: 0 0 100%` → `flex: 0 0 auto; width: auto` so the gear inlines with the critical strip + 📊 toggle - when the header is also collapsed (`.is-collapsed:has(.live-controls.is-collapsed)`), zero the vertical padding so the strip hugs the 48px tap targets Result: collapsed mobile panel = single ~50px row, three icons inline. Expanded mobile = full toggle list (149px). Desktop unchanged (83px). Why Option C over A/B: a packet-watching mobile user keeps the map dominant and reaches for the gear when they want filters. The compact strip preserves both the WS-down red beacon (always visible) and the pkt count, with one-tap access to expand either body. Does not reintroduce #1204 (counter still attached to header) or #1205 (toggles still children of `#liveHeader`). Fixes #1220 --------- Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com> |
||
|
|
24f277e5c6 |
fix(#1221): VCR LED clock in-row with controls and unclipped on mobile (#1222)
Red commit:
|
||
|
|
11d2026bb1 |
feat(startup): hot startup — load hotStartupHours synchronously, fill retentionHours in background (#1187)
Closes #1183 ## Summary - Adds `packetStore.hotStartupHours` config key (float64, default 0 = disabled). When set, `Load()` loads only that many hours of data synchronously, reducing startup time on large DBs. Background goroutine fills the remaining `retentionHours` window in daily chunks after startup completes. - A background goroutine (`loadBackgroundChunks`) fills the remaining `retentionHours` window in daily chunks after startup completes. Analytics indexes are rebuilt once at the end. - `QueryPackets` and `QueryGroupedPackets` check `oldestLoaded` and fall back to `db.QueryPackets()` for any query whose `Since`/`Until` predates the in-memory window — covering days 8–30 permanently (beyond `retentionHours`) and the background-fill gap during startup. - `/api/perf` gains `hotStartupHours`, `backgroundLoadComplete`, and `backgroundLoadProgress` fields inside `packetStore` so operators can monitor the fill. ### Drive-by fixes - E2E: added `gotoPackets` navigation helper used across packet-related tests - E2E: rewrote stripe assertion to check per-row stripe parity rather than a fragile computed-style comparison - E2E: theme test updated to use `#/home` as the initial route (was `#/`) - `db.go`: removed the RFC3339→unix-timestamp subquery path in `buildTransmissionWhere`; `t.first_seen` is now always compared directly as a string for both RFC3339 and non-RFC3339 inputs ## Configuration ```json "packetStore": { "retentionHours": 168, "hotStartupHours": 24 } ``` `hotStartupHours: 0` (default) preserves existing behavior exactly. Recommended for large DBs to reduce startup time; set to 0 to disable (loads full retentionHours at startup, legacy behavior). ## Test plan - [x] `TestHotStartupConfig_Clamp` — clamping when `hotStartupHours > retentionHours` - [x] `TestHotStartupConfig_ZeroIsDisabled` — zero leaves feature disabled - [x] `TestHotStartup_LoadsOnlyHotWindow` — only hot-window packets in memory after `Load()` - [x] `TestHotStartup_DisabledWhenZero` — all retention packets loaded when disabled - [x] `TestHotStartup_loadChunk_AddsOlderData` — chunk merges correctly, ASC order maintained - [x] `TestHotStartup_BackgroundFillsToRetention` — background goroutine fills to `retentionHours` - [x] `TestHotStartup_ChunkErrorRecovery` — chunk SQL failure logged and skipped, loop terminates - [x] `TestHotStartup_SQLFallback_TriggeredForOldDate` — query before `oldestLoaded` routes to SQL - [x] `TestHotStartup_SQLFallback_NotTriggeredForRecentDate` — recent query stays in-memory - [x] `TestHotStartup_PerfStats` — new fields present in `GetPerfStoreStats()` (backs the perf endpoint) - [x] `TestHotStartup_PerfStoreHTTP` — HTTP-level: GET /api/perf returns `hotStartupHours`, `backgroundLoadComplete`, `backgroundLoadProgress` in `packetStore` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: CoreScope Bot <bot@corescope.local> |
||
|
|
4925770aa4 |
fix(#1207): empty-state placeholder for Live Feed panel (no more orphan chrome) (#1210)
Red commit: `6c28227884a1e79e277653465028365dc0863171` — CI: https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1207 Fixes #1207 ## Diagnosis The Live Map page renders `#liveFeed` (bottom-left panel) with two header buttons — `◫` (panel-corner-btn) and `✕` (feed-hide-btn) — but its `.panel-content` body has zero children on first paint, before any packets have been ingested via WebSocket. The user-reported "X + book icons, no content" is exactly these two header buttons sitting on an empty body. **Verdict:** intended panel, missing content due to a data race — the chrome mounts in HTML before the WS pushes its first packet. Not orphaned, not a leftover from #1186. ## Fix - Always render a persistent `.live-feed-empty` placeholder ("Waiting for packets…") inside `#liveFeed .panel-content`. - CSS hides it via `.live-feed .panel-content:has(.live-feed-item) .live-feed-empty { display: none; }` when real feed items exist. - `rebuildFeedList` re-adds the placeholder defensively after a wipe; eviction loop counts `.live-feed-item` only so the placeholder is never trimmed out. All colors via CSS variables (`var(--text-muted)`). ## Test (RED → GREEN) - **RED** `6c28227884a1e79e277653465028365dc0863171` — `test-e2e-playwright.js` adds a new test ("#1207 Live Feed panel never renders as empty chrome") that wipes `.live-feed-item` children to simulate the empty state and asserts the panel body has visible text or children. Fails on master. - **GREEN** `a5af80960ac42759ec83fd5ca5a72e81856228d4` — adds the placeholder; test now passes. ## Acceptance criteria - [x] No empty panel chrome visible on Live Map page - [x] Panel renders "Waiting for packets…" while feed is empty - [x] CSS auto-hides placeholder when packets arrive - [x] E2E assertion in `test-e2e-playwright.js` enforces non-empty `.panel-content` on `#liveFeed` ## Files - `public/live.js` — HTML markup + `rebuildFeedList` re-add + eviction-loop guard - `public/live.css` — `.live-feed-empty` style + `:has()` hide rule - `test-e2e-playwright.js` — regression test --------- Co-authored-by: clawbot <clawbot@kpabap.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
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> |
||
|
|
eae1b915ca |
feat: load Aldrich webfont for #1137 logo SVG (#1138)
Adds Aldrich webfont so the merged #1137 logo renders in the intended typeface. ## Problem The inline SVG logo merged in #1137 declares `font-family="Aldrich, monospace"` in `public/index.html` and `public/home.js`, but the page never loaded the Aldrich font face. Browsers silently fell back to monospace. ## Fix Self-hosted webfont: - `public/fonts/aldrich-regular.woff2` — Regular 400, ~16KB, downloaded from Google Fonts (latin subset). Self-hosted to avoid third-party CDN dependency, privacy concern, and FOUT delay. - `@font-face` declaration added at the top of `public/style.css` with `font-display: swap`. Aldrich only ships in 400; the SVG `font-weight="700"` on the wordmark synthesizes bold (matches the design intent of #1137). ## TDD - Red commit: E2E test asserting `document.fonts.check('1em Aldrich')` is true and the navbar SVG `<text>` `font-family` contains "Aldrich". Without the font face declaration, both assertions fail on an assertion (not a build error). - Green commit: adds the woff2 + `@font-face` rule, both assertions pass. ## Files - `public/fonts/aldrich-regular.woff2` (new, 16460 bytes) - `public/style.css` — `@font-face` rule - `test-e2e-playwright.js` — new test --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
50676d5e65 |
fix(live): #1110 node filter — autocomplete, theming, no reload (#1113)
## Summary Fixes the broken **Filter by node** input on the Live page. The previous implementation used a native `<datalist>` (no consistent styling, no real autocomplete UX), only applied on `change` (Enter), and mutated `location.hash` on commit — which the SPA router treated as a navigation, triggering a full re-init. ## What changed - **Markup** (`public/live.js`): replaces the `<datalist>` with a styled custom `#liveNodeFilterDropdown` and adds combobox/listbox ARIA wiring. - **Styling** (`public/live.css`): new `.live-node-filter-input` rules use `color-mix` on `var(--text)` for the background and `var(--border)` / `var(--text)` for border + foreground — fully theme-aware. Dropdown uses `var(--surface-1)` + `var(--border)`. - **Behavior**: 200 ms debounced `/api/nodes/search` call as the user types. Suggestions render with name + 8-char pubkey prefix. Clicking a suggestion (`mousedown` so it beats blur) sets the filter to the pubkey. - **No reload**: `applyFilterFromInput` and the clear button now use `history.replaceState` instead of mutating `location.hash`, so the SPA router never re-runs and the page never reloads. Enter is `preventDefault`-ed and either selects the highlighted suggestion or commits the typed text. - **Keyboard**: ArrowUp/Down navigate suggestions, Esc closes, Enter selects. ## TDD Per `AGENTS.md`, the failing E2E test landed first (commit `74f3e92`), then the fix made it green (commit `a5c5c65`). The test file `test-1110-live-filter.js` (and an integrated block in `test-e2e-playwright.js`) asserts: 1. The input's computed `background-color` is **not** hardcoded white when `data-theme="dark"` is set. 2. The input is not vastly larger than the surrounding toolbar row. 3. Typing `"te"` shows a visible `#liveNodeFilterDropdown` with at least one `.live-node-filter-option`. 4. Clicking a suggestion sets `_liveGetNodeFilterKeys()` to a non-empty list **without** reloading the page (verified via a `window.__m` marker that survives) and **without** navigating away from `#/live`. 5. Pressing **Enter** in the filter input never reloads or navigates. ### How to run the E2E ``` go build -o /tmp/corescope-server ./cmd/server /tmp/corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public & CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:13581 \ node test-1110-live-filter.js # 4/4 passed ``` ## Acceptance criteria from #1110 - [x] Filter input visually matches Live page toolbar (theme-aware bg, border, padding) - [x] Typing 1+ characters shows dropdown of matching node names - [x] Selecting a suggestion filters the live feed immediately - [x] Clearing input restores unfiltered view - [x] No page reload on any interaction with the input - [x] E2E test asserts: type → suggestions appear → click suggestion → feed filters → no navigation Fixes #1110 --------- Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com> |
||
|
|
52bb07d6c1 |
feat(#1056): fluid tables + +N hidden pill (packets/nodes/observers) (#1099)
## Summary Implements priority-based responsive column hiding for the three primary data tables (Packets, Nodes, Observers) per the parent task #1050 acceptance criteria, with a clickable **+N hidden** pill in the table header to reveal collapsed columns. ## Approach - New `TableResponsive` helper (defined once at the top of `packets.js`, exposed on `window`) classifies `<th data-priority="N">` cells: - `1` = always visible - `2` = hide when viewport ≤ 1280 - `3` = hide ≤ 1080 - `4` = hide ≤ 900 - `5` = hide ≤ 768 - Higher priority numbers drop first. The matching `<td>` cells in `tbody` are tagged via `.col-hidden` (colspan-aware mapping). - A `.col-hidden-pill` `<button>` is appended to the last visible `<th>`. Clicking it sets a per-table reveal flag and clears all hidden classes. Re-runs on `window.resize` (debounced) and a `ResizeObserver` on the wrapping element. - Each of `packets.js` / `nodes.js` / `observers.js` wraps its primary table in `.table-fluid-wrap` and calls `TableResponsive.register` after initial render. - `style.css` removes legacy `min-width: 720px / 480px` floors on the primary tables (which forced horizontal scroll) and lets columns flex via `table-layout: auto` with `.col-time` switched to `clamp(72px, 8vw, 108px)`. Per-column priorities chosen so identifier columns stay visible (Time/Hash/Type/Name/Status) while numeric/secondary columns collapse first. ## Files changed (matches Hard rules — only these) - `public/packets.js` (`#pktTable` + `TableResponsive` helper) - `public/nodes.js` (`#nodesTable`) - `public/observers.js` (`#obsTable`) - `public/style.css` (table sections only) - `test-table-fluid-e2e.js` (new E2E) ## E2E `BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js` — covers all three tables at 768/1080/1440 viewports, asserting: - No horizontal table overflow within `.table-fluid-wrap` - Visible `+N hidden` pill at narrow widths with the count `N` matching the number of `th.col-hidden` cells - Clicking the pill clears all `.col-hidden` classifiers (reveals every column) ## Manual verification in openclaw browser (local fixture server) | Page | Viewport | Hidden | Pill | |-----------|---------:|-------:|--------------| | observers | 768 | 8 | `+8 hidden` | | packets | 768 | 7 | `+7 hidden` | | packets | 1080 | 4 | `+4 hidden` | | nodes | 768 | 3 | `+3 hidden` | | nodes | 1440 | 0 | (no pill) | Pill click verified to reveal all columns. ## TDD - Red commit: `5ad7573` — failing E2E (no `.col-hidden-pill` exists yet) - Green commit: `7780090` — implementation; test passes manually against fixture server. Fixes #1056 --------- Co-authored-by: openclaw-bot <bot@openclaw.dev> Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
36ee71d17e |
feat(#1085): fold Roles page into Analytics tab (#1088)
Red commit:
|
||
|
|
282074b19d |
feat(#1034): wire QR generate + scan into channel modal (PR 3/3) (#1081)
## Summary **PR 3/3 of #1034** — wires the existing `window.ChannelQR` module (PR2 #1035) into the existing channel modal placeholders (PR1 #1037). ### Changes **`public/channels.js`** - **Generate handler** (`#chGenerateBtn`): replaced the "QR coming in next update" placeholder text with a real call to `window.ChannelQR.generate(label || channelName, keyHex, qrOut)`. Renders QR canvas + `meshcore://channel/add?...` URL + Copy Key inline into `#qr-output`. - **Scan handler** (`#scan-qr-btn`): removed `disabled` attribute, refreshed title, and added a click handler that calls `window.ChannelQR.scan()`. On success it populates `#chPskKey` (from `result.secret`) and `#chPskName` (from `result.name`); on cancel it's a no-op; on error it surfaces the message via `#chPskError`. The Share button on sidebar entries was already wired to `ChannelQR.generate` in PR1 (no change needed). ### TDD 1. **Red commit** (`178020b`): `test-channel-qr-wiring.js` — 12 assertions, 7 failed against the placeholder code (Generate handler still printed "coming in next update", scan button still disabled). 2. **Green commit** (`e708f3f`): wiring added → all 12 assertions pass. ### E2E (rule 18) `test-e2e-playwright.js` gains 3 Playwright tests (run against the live Go server with fixture DB in CI): - Generate → asserts `#qr-output canvas` and the `meshcore://channel/add` URL appear after the click. - Scan button is enabled (no `disabled` attribute). - Stubs `ChannelQR.scan` to return `{name, secret}`, clicks the button, asserts `#chPskKey` + `#chPskName` are populated. ### CI registration Added `node test-channel-qr-wiring.js` and `node test-channel-modal-ux.js` to the JS unit-test step in `.github/workflows/deploy.yml` (and `test-all.sh`). ### Closes Closes #1034 (final PR in the redesign series). --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> |
||
|
|
aa3d26f314 |
fix(nav): stop nav bar from jumping when Live is selected (#1046) (#1078)
## Summary The `🔴 Live` nav link could wrap onto two lines at certain viewport widths once it became the `.active` link, which grew `.nav-link`'s height and made the whole `.top-nav` "hop" the instant Live was selected (issue #1046). Adding `white-space: nowrap` to the base `.nav-link` rule keeps every nav label on a single line at every breakpoint (default desktop + the 768–1279px and <768px responsive overrides), eliminating the jump. ## Changes - `public/style.css` — `white-space: nowrap` on `.nav-link`. - `test-e2e-playwright.js` — new assertion at viewport 1115px (the width in the issue screenshots) that: - computed `white-space` prevents wrapping - the Live link renders on a single line in both states - `.top-nav` height does not change when `.active` is toggled ## TDD - Red commit `ba906a5` — test added, fails because base `.nav-link` has no `white-space` rule (default `normal`). - Green commit `51906cb` — single-line CSS fix makes the test pass. Fixes #1046 --------- Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
417b460fa0 |
feat(css): fluid scaffolding — clamp() spacing/type/container tokens (#1054) (#1066)
## Summary Lands the **fluid CSS foundation** for the responsive scaffolding effort (parent #1050). Pure additive change to the top of `public/style.css` — no component CSS touched. ## What changed ### New tokens in `:root` - **Spacing scale** — `--space-xs … --space-2xl` via `clamp()`. 1440px targets match the prior hardcoded `4 / 8 / 16 / 24 / 32 / 48` px values to within ~1px. - **Type scale** — `--fs-sm … --fs-2xl` via `clamp(min, vw-based, max)`. Floors keep text readable at 768px; caps prevent runaway growth at 2560px+. - **Radii** — `--radius-sm/md/lg` via `clamp()`. - **Container layout** — `--gutter` (`clamp()`) and `--content-max` (`min(100% - 2*gutter, 1600px)`) for fluid horizontal layout without media queries. ### Base consumption - `html, body` now sets `font-size: var(--fs-md)`. ### Parallel-work safety - Added `FLUID SCAFFOLDING` section header at the top. - Added `COMPONENT STYLES` section header marking where the rest of the file (nav, tables, charts, map, packets, analytics …) begins. Sibling tasks 1050-3..6 / 1052-* edit inside that region and won't conflict with this PR. ## TDD - **Red:** `2d6f90a` — `test-fluid-scaffolding.js` asserts the new tokens exist with `clamp()`/`min()`, that `html, body` consumes `--fs-md`, and that the section marker is present. Fails on assertions (15 failed, 0 passed). - **Green:** `7b4d59b` — implementation in `public/style.css`. All 15 assertions pass. ## Acceptance criteria - [x] Fluid spacing scale `--space-xs..--space-2xl` via `clamp()` - [x] Fluid type scale `--fs-sm..--fs-2xl` via `clamp()` - [x] Replace base body font-size with the new token - [x] Container layout vars `--content-max`, `--gutter` via `min()`/`clamp()` - [x] No component CSS edits (only `:root`, `html`, `body`) - [x] No visual regression at 1440px (token targets numerically match prior px values) ## Notes for reviewers - Pre-existing `test-frontend-helpers.js` failure on master is unrelated (`nodesContainer.setAttribute is not a function`) and not introduced here. - `--content-max` uses `min(100% - 2*gutter, 1600px)` — the `100% - …` arm wins on small viewports and guarantees a gutter always remains. Fixes #1054 --------- Co-authored-by: clawbot <bot@corescope.local> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
38703c75e6 |
fix(e2e): make Nodes WS auto-update test deterministic (#1051)
## Problem
The Playwright E2E test `Nodes page has WebSocket auto-update`
(`test-e2e-playwright.js:259`) has flaked 7+ times this session,
blocking CI. Failure mode:
```
page.waitForSelector: Timeout 10000ms exceeded
waiting for locator('table tbody tr') to be visible
```
## Root cause
The test navigates to `/#/nodes`, waits for `[data-loaded="true"]`
(passes), then waits for `table tbody tr` (10s, fails intermittently).
Rows in this code path only appear via WebSocket push — which is
timing-dependent in CI (no guaranteed live MQTT feed within the 10s
window).
## Fix
Drop the `table tbody tr` wait. This test's contract is **WS
infrastructure existence**, not data delivery:
- `#liveDot` element present
- `onWS` / `offWS` globals defined
- Best-effort connected-state check (already tolerant of failure)
All those assertions are deterministic post-DOMContentLoaded. Initial
table population is already covered by the preceding `Nodes page loads
with data` test.
## Coverage
No coverage loss — the WS infra assertions are unchanged. Only the
timing-dependent row-presence wait is removed.
## TDD note
This is a test-fix, not a behavior change. The "red" is the existing
intermittent CI failure; the "green" is this commit removing the flaky
wait. No production code touched.
Co-authored-by: meshcore-bot <bot@meshcore.local>
|
||
|
|
51b9fed15e |
feat(roles): /#/roles page + /api/analytics/roles endpoint (Fixes #818) (#1023)
## Summary Implements `/#/roles` per QA #809 §5.4 / issue #818. The page previously showed "Page not yet implemented." ### Backend - New `GET /api/analytics/roles` returns `{ totalNodes, roles: [{ role, nodeCount, withSkew, meanAbsSkewSec, medianAbsSkewSec, okCount, warningCount, criticalCount, absurdCount, noClockCount }] }`. - Pure `computeRoleAnalytics(nodesByPubkey, skewByPubkey)` does the bucketing/aggregation — no store/lock dependency, fully unit-testable. - Roles are normalised (lowercased + trimmed; empty bucketed as `unknown`). ### Frontend - New `public/roles-page.js` renders a distribution table: count, share, distribution bar, w/ skew, median |skew|, mean |skew|, severity breakdown (OK / Warning / Critical / Absurd / No-clock). - Registered as the `roles` page in the SPA router and linked from the main nav. - Auto-refreshes every 60 s, with a manual refresh button. ### Tests (TDD) - **Red commit** (`9726d5b`): two assertion-failing tests against a stub `computeRoleAnalytics` that returns an empty result. Compiles, runs, fails on `TotalNodes = 0, want 5` and `len(Roles) = 0, want 1`. - **Green commit** (`7efb76a`): full implementation, route wiring, frontend page + nav, plus E2E test in `test-e2e-playwright.js` covering both the empty-state contract (no "Page not yet implemented" placeholder) and the populated-table case (header columns, body rows, API response shape). ### Verification - `go test ./cmd/server/...` green. - Local server with the e2e fixture: `GET /api/analytics/roles` returns `{"totalNodes":200,"roles":[{"role":"repeater","nodeCount":168,...},{"role":"room","nodeCount":23,...},{"role":"companion","nodeCount":9,...}]}`. Fixes #818 --------- Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
a56ee5c4fe |
feat(analytics): selectable timeframes via ?window/?from/?to (#842) (#1018)
## Summary Selectable analytics timeframes (#842). Adds backend support for `?window=1h|24h|7d|30d` and `?from=&to=` on the three main analytics endpoints (`/api/analytics/rf`, `/api/analytics/topology`, `/api/analytics/channels`), and a time-window picker in the Analytics page UI that drives them. Default behavior with no query params is unchanged. ## TDD trail - Red: `bbab04d` — adds `TimeWindow` + `ParseTimeWindow` stub and tests; tests fail on assertions because the stub returns the zero window. - Green: `75d27f9` — implements `ParseTimeWindow`, threads `TimeWindow` through `compute*` loops + caches, wires HTTP handlers, adds frontend picker + E2E. ## Backend changes - `cmd/server/time_window.go` — full `ParseTimeWindow` (`?window=` aliases + `?from=/&to=` RFC3339 absolute range; invalid input → zero window for backwards compatibility). - `cmd/server/store.go` — new `GetAnalytics{RF,Topology,Channels}WithWindow` wrappers; `compute*` loops skip transmissions whose `FirstSeen` (or per-obs `Timestamp` for the region+observer slice) falls outside the window. Cache key composes `region|window` so different windows do not poison each other. - `cmd/server/routes.go` — handlers call `ParseTimeWindow(r)` and dispatch to the `*WithWindow` methods. ## Frontend changes - `public/analytics.js` — new `<select id="analyticsTimeWindow">` rendered under the region filter (All / 1h / 24h / 7d / 30d). Selecting an option triggers `loadAnalytics()` which appends `&window=…` to every analytics fetch. ## Tests - `cmd/server/time_window_test.go` — covers all aliases, absolute range, no-params backwards compatibility, `Includes()` bounds, and `CacheKey()` distinctness. - `cmd/server/topology_dedup_test.go`, `cmd/server/channel_analytics_test.go` — updated callers to pass `TimeWindow{}`. ## E2E (rule 18) `test-e2e-playwright.js:592-611` — opens `/#/analytics`, asserts the picker is rendered with a `24h` option, then asserts that selecting `24h` triggers a network request to `/api/analytics/rf?…window=24h`. ## Backwards compatibility No params → zero `TimeWindow` → original code paths (no filter, region-only cache key). Verified by `TestParseTimeWindow_NoParams_BackwardsCompatible` and by the existing analytics tests still passing unchanged on `_wt-fix-842`. Fixes #842 --------- Co-authored-by: you <you@example.com> Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
df69a17718 |
feat(#772): short pubkey-prefix URLs for mesh sharing (#1016)
## Summary Fixes #772 — adds a short-URL form for node detail pages so operators can paste node links into a mesh chat without bringing along a 64-hex-char public key. ## Approach **Pubkey-prefix resolution** (no allocator, no lookup table). - The SPA hash route `#/nodes/<key>` already accepts whatever pubkey-shaped string the user pastes; the front end forwards it to `GET /api/nodes/<key>`. - When that lookup misses **and** the path is 8..63 hex chars, the backend now calls `DB.GetNodeByPrefix` and: - returns the matching node when exactly one node has that prefix, - returns **409 Conflict** when multiple nodes share the prefix (with a "use a longer prefix" hint), - falls through to the existing 404 otherwise. - 8 hex chars = 32 bits of entropy, which is enough for fleets in the low thousands. Operators can extend to 10–12 chars if collisions become common. - The full-screen node detail card gets a new **📡 Copy short URL** button that copies `…/#/nodes/<first 8 hex chars>`. ### Why not an opaque ID table (`/s/<id>`)? Considered and rejected: - Needs persistence + an allocator + cleanup story. - IDs aren't self-describing — operators can't sanity-check them. - IDs don't survive a DB rebuild. - 32 bits of pubkey already buys us collision resistance with zero moving parts. If the directory grows past the point where 8-char prefixes routinely collide, we can extend the minimum length without changing the URL shape. ## Changes - `cmd/server/db.go` — new `GetNodeByPrefix(prefix)` returning `(node, ambiguous, error)`. Validates hex; rejects <8 chars; `LIMIT 2` to detect collisions cheaply. - `cmd/server/routes.go` — `handleNodeDetail` falls back to prefix resolution; canonicalizes pubkey downstream; emits 409 on ambiguity; honors blacklist on the resolved pubkey. - `public/nodes.js` — adds **📡 Copy short URL** button + handler on the full-screen node detail card. - `cmd/server/short_url_test.go` — Go tests (red-then-green). - `test-e2e-playwright.js` — E2E: navigates via prefix-only URL and asserts the new button surfaces. ## TDD evidence - Red commit: `2dea97a` — tests added with a stub `GetNodeByPrefix` returning `(nil, false, nil)`. All four assertions failed (assertion failures, not build errors): expected node got nil; expected ambiguous=true got false; route 404 vs expected 200/409. - Green commit: `9b8f146` — implementation lands; `go test ./...` passes locally in `cmd/server`. ## Compatibility - Existing 64-char pubkey URLs are untouched (exact lookup runs first). - Blacklist is enforced both on the raw input and on the resolved pubkey. - No new config knobs. ## What I did **not** touch - `cmd/server/db_test.go`, other route tests — unchanged. - Packet-detail short URLs (issue scopes nodes; revisit in a follow-up if asked). Fixes #772 --------- Co-authored-by: clawbot <bot@corescope.local> |
||
|
|
d3920f66e9 |
fix(test): correct leaflet-container selector in geofilter E2E (#1017)
## Summary
Fixes the `Geofilter draft: save → reload → load → download round-trip`
Playwright E2E test that was failing on master with a 10s
`waitForFunction` timeout.
## Root cause
`test-e2e-playwright.js:2270` used the descendant combinator `'#map
.leaflet-container'`, expecting a child element. Leaflet's
`L.map('map')` adds the `leaflet-container` class **directly to the
`#map` element itself**, so the descendant query never matched and the
wait hung until timeout.
## Fix
Single-character edit: drop the space between `#map` and
`.leaflet-container` so the selector matches the same element
(`#map.leaflet-container`).
```diff
-await page.waitForFunction(() => window.L && document.querySelector('#map .leaflet-container'), { timeout: 10000 });
+await page.waitForFunction(() => window.L && document.querySelector('#map.leaflet-container'), { timeout: 10000 });
```
The working `Map page loads with markers` test at line 289 already uses
the bare `.leaflet-container` selector, confirming the convention.
## TDD exemption
**Test-fix exemption (per AGENTS.md TDD rules):** this PR fixes an
existing failing test assertion with no production behavior change. The
"red" state is current master (test currently times out in CI run
25287101810). No production code is touched; the geofilter feature
itself works (Leaflet initializes correctly — the test just never
observed it due to the broken selector). Going forward, the test
continues to gate the geofilter draft round-trip behavior.
## Verification
- CI Playwright E2E job should now reach past line 2270 and exercise the
geofilter buttons (`#btnSaveDraft`, `#btnLoadDraft`, `#btnDownload`).
- No other tests modified.
Co-authored-by: you <you@example.com>
|
||
|
|
4d043579f8 |
feat: geofilter draft save (localStorage) + downloadable config snippet (#1006)
## Issue Closes #819 ## Summary Adds Save Draft / Load Draft / Download buttons to `/geofilter-builder.html` so operators can: - Persist their work-in-progress polygon across sessions (localStorage) - Reload it later to continue editing - Download a ready-to-paste `geo_filter` JSON snippet for `config.json` ## Implementation - New module `public/geofilter-draft.js` exposes `GeofilterDraft` global with `saveDraft / loadDraft / clearDraft / buildConfigSnippet / downloadConfig`. - Builder HTML wires three new buttons; updates the help text to document the new flow. ## TDD - Red commit: `b0a1a4c` (tests fail — module doesn't exist) - Green commit: `a717f33` (implementation added, all tests pass) ## How to test 1. Open `/geofilter-builder.html` 2. Click 3+ points on the map 3. Click "Save Draft" — reload page — click "Load Draft" → polygon restored 4. Click "Download" → `geofilter-config-snippet.json` downloaded with correct format --- E2E assertion added: test-e2e-playwright.js:2264 --------- Co-authored-by: you <you@example.com> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
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> |
||
|
|
7bb5ff9a7f |
fix(e2e): tag flying-packet polyline so test selector doesn't pick up geofilter polygons (#953)
## Bug Master CI failing on `Map trace polyline uses hash-derived color when toggle ON`. The test selector `path.leaflet-interactive` was too broad — it matched **geofilter region polygons** (`L.polygon` calls in `live.js:1052`/`map.js:327`), which are styled with theme variables, not `hsl()`. None of those polygons have an `hsl(` stroke, so the assertion failed even though the actual flying-packet polylines DO use hash colors correctly. ## Fix 1. Tag flying-packet polylines with a dedicated class `live-packet-trace` (`public/live.js:2728`). 2. Update the test selector to target that class specifically. 3. Treat "no flying-packet polylines drawn in the test window" as SKIP (not fail) — animation may not trigger in 3s. ## Verification (rule 18) - Read implementation at `live.js:2724-2729`: polyline color IS set from `hashFill` when toggle is ON. The implementation is correct. - Read polygon callers at `live.js:1052` (geofilter regions) — confirmed they share the same `path.leaflet-interactive` class. - The test was selecting wrong DOM nodes; fix narrows to dedicated class. No code logic changed — only DOM tagging + test selector. Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
b9758111b0 |
feat(hash-color): bright vivid fill + dark outline + live feed/polyline surfaces (#951)
## Hash-Color: Bright Vivid Fill + Dark Outline + Extended Surfaces Follow-up to #948 (merged). Revises the hash-color algorithm for better perceptual discrimination and extends hash coloring to additional Live page surfaces. ### Algorithm Changes (`public/hash-color.js`) - **Hue**: bytes 0-1 (16-bit → 0-360°) — unchanged - **Saturation**: byte 2 (55-95%) — NEW, was fixed 70% - **Lightness**: byte 3 (light 50-65%, dark 55-72%) — NEW, was fixed L=30/38/65 - **Outline** (`hashToOutline`): same-hue dark color (L=25% light, L=15% dark) — NEW - Sentinel threshold raised to 8 hex chars (need 4 bytes of entropy) - Drops WCAG fill-darkening approach — outline carries contrast instead ### Live Page Updates (`public/live.js`) - **Dot marker**: uses `hashToOutline()` for stroke (was TYPE_COLOR) - **Polyline trace**: uses hash fill color (unified dot + trace by hash) - **Feed items**: 4px `border-left` stripe matching packets table ### Test Updates - `test-hash-color.js`: 32 tests (S variability, L variability, outline < fill, same hue, pairwise distance) - `test-e2e-playwright.js`: 2 new assertions (feed stripe, polyline hsl stroke) ### Verification - 20 real advert hashes from fixture DB: all produce unique hues (20/20) - Pairwise HSL distance: avg=0.51, min=0.04 - Go server built and run against fixture DB — HTML serves updated module - VM sandbox render-check confirms distinct vivid fills with darker outlines Closes #946 §2.10/§2.11 scope extension. --------- Co-authored-by: you <you@example.com> Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
0a9a4c4223 |
feat(live + packets): color packet markers by hash (#946) (#948)
## Summary Implements #946 — deterministic HSL coloring of packet markers by hash for visual propagation tracing. ### What's new 1. **`public/hash-color.js`** — Pure IIFE (`window.HashColor.hashToHsl(hashHex, theme)`) deriving hue from first 2 bytes of packet hash. Theme-aware lightness with WCAG ≥3.0 contrast against `--content-bg` (`#f4f5f7` light / `#0f0f23` dark, `style.css:32,55`). Green/yellow zone (hue 45°-195°) uses L=30% in light theme to maintain contrast. 2. **Live page dots + contrails** — `drawAnimatedLine` fills the flying dot and tints the contrail polyline with the hash-derived HSL when toggle is ON. Ghost-hop dots remain grey (`#94a3b8`). Matrix mode path (`drawMatrixLine`) is untouched. 3. **Packets table stripe** — `border-left: 4px solid <hsl>` on `<tr>` in both `buildGroupRowHtml` (group + child rows) and `buildFlatRowHtml`. Absent when toggle OFF. 4. **Toggle UI** — "Color by hash" checkbox in `#liveControls` between Realistic and Favorites. Default ON. Persisted to `localStorage('meshcore-color-packets-by-hash')`. Dispatches `storage` event for cross-tab sync. Packets page listens and re-renders. ### Performance - `hashToHsl` is O(1) — two `parseInt` calls + arithmetic. No allocation beyond the result string. - Called once per `drawAnimatedLine` invocation (not per animation frame). - Packets table: called once per visible row during render (existing virtualization applies). ### Tests - `test-hash-color.js`: 16 unit tests — purity, theme split, yellow-zone clamp, sentinel, variability (anti-tautology gate), WCAG sweep (step 15° both themes). - `test-packets.js`: 82 tests still passing (no regression). - `test-e2e-playwright.js`: 4 new E2E tests — toggle presence/default, persistence across reload, table stripe present when ON, absent when OFF. ### Acceptance criteria addressed All items from spec §6 implemented. TYPE_COLORS retained on borders/lines. Ghost hops stay grey. Matrix mode suppressed. Cross-tab storage event dispatched. Closes #946 --------- Co-authored-by: you <you@example.com> Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
e460932668 |
fix(store): apply retentionHours cutoff in Load() to prevent OOM on cold start (#917)
## Problem `Load()` loaded all transmissions from the DB regardless of `retentionHours`, so `buildSubpathIndex()` processed the full DB history on every startup. On a DB with ~280K paths this produces ~13.5M subpath index entries, OOM-killing the process before it ever starts listening — causing a supervisord crash loop with no useful error message. ## Fix Apply the same `retentionHours` cutoff to `Load()`'s SQL that `EvictStale()` already uses at runtime. Both conditions (`retentionHours` window and `maxPackets` cap) are combined with AND so neither safety limit is bypassed. Startup now builds indexes only over the retention window, making startup time and memory proportional to recent activity rather than total DB history. ## Docs - `config.example.json`: adds `retentionHours` to the `packetStore` block with recommended value `168` (7 days) and a warning about `0` on large DBs - `docs/user-guide/configuration.md`: documents the field and adds an explicit OOM warning ## Test plan - [x] `cd cmd/server && go test ./... -run TestRetentionLoad` — covers the retention-filtered load: verifies packets outside the window are excluded, and that `retentionHours: 0` still loads everything - [x] Deploy on an instance with a large DB (>100K paths) and `retentionHours: 168` — server reaches "listening" in seconds instead of OOM-crashing - [x] Verify `config.example.json` has `retentionHours: 168` in the `packetStore` block - [x] Verify `docs/user-guide/configuration.md` documents the field and warning 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com> |
||
|
|
f2689123f3 |
fix(geobuilder): wrap longitude to [-180,180] to fix southern hemisphere polygons (#925)
## Summary - Fixes #912 — geofilter-builder generates out-of-range longitudes for southern hemisphere locations - Root cause: Leaflet's `latlng.lng` is unbounded; panning from Europe to Australia produces values like `-210` instead of `150` - Fix: call `latlng.wrap()` in `latLonPair()` to normalise longitude to `[-180, 180]` before writing the config JSON ## Details When the user opens the builder (default view: Europe, `[50.5, 4.4]`) and pans east to Australia, Leaflet tracks the cumulative pan offset and returns `lng = 150 - 360 = -210` to keep the path continuous. The builder was passing that raw value straight into the output JSON, producing coordinates that fall outside any valid bounding box. `L.LatLng.wrap()` is Leaflet's built-in normalisation method — collapses any longitude to `[-180, 180]` with no loss of precision. ## Test plan - [x] Open the builder, navigate to NSW Australia, place a polygon — confirm longitudes are `~141`–`154`, not `~-219`–`-206` - [x] Repeat for a northern hemisphere location (e.g. Belgium) — confirm output is unchanged - [x] Paste the generated config into CoreScope — confirm nodes appear on Maps and Live view 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com> |
||
|
|
9293ff408d |
fix(customize): skip panel re-render while a text field has focus (#927)
## Summary - `_debouncedWrite()` was calling `_refreshPanel()` 300ms after every keystroke - `_refreshPanel()` sets `container.innerHTML`, destroying the focused input element - On mobile, losing the focused input collapses the virtual keyboard after each keypress Guard the `_refreshPanel()` call so it is skipped when `document.activeElement` is inside the panel. The pipeline (`_runPipeline`) still runs immediately — CSS updates apply. Override dots update on the next natural re-render (tab switch, dark-mode toggle, panel reopen). ## Root cause `customize-v2.js` → `_debouncedWrite()` → `_refreshPanel()` → `_renderPanel()` → `container.innerHTML = ...` ## Test plan - [ ] New Playwright E2E test: open Customize, focus a text field, type, wait 500ms past debounce — asserts input element is still connected to DOM and focus remains inside panel - [ ] Manual: open Customize on mobile (or DevTools mobile emulation), type in Site Name — keyboard must not collapse after each character Fixes #896 |
||
|
|
8c3b2e2248 |
test(e2e): retry click on table rows when handles detach (#943)
## Problem E2E test `Node detail loads` intermittently fails with: > elementHandle.click: Element is not attached to the DOM (e.g. PR #938 CI run job 73889426640.) Same flake class as #ngStats hydration race fixed in #940. ## Root cause ```js const firstRow = await page.$('table tbody tr'); await firstRow.click(); ``` Between the `$()` and `.click()`, the nodes table re-renders from a WebSocket push. The captured handle is detached from the new DOM, click throws. ## Fix Switch to a selector-based click with a small retry loop (3 attempts × 200ms backoff), so a detach mid-attempt re-resolves a fresh element. Test logic unchanged; just defensive against re-render between query and click. Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
6273a8797b |
test(e2e): wait for #ngStats hydration before counting cards (#940)
## Problem E2E test "Analytics Neighbor Graph tab renders canvas and stats" intermittently fails with `Neighbor Graph stats should have >=3 cards, got 0` (e.g. run 25185836669). The same suite passes on neighboring runs (master + PR #939) within minutes. The failure correlates with timing/load, not code change. ## Root cause `#ngStats` cards render asynchronously after `#ngCanvas` mounts. The test waits for the canvas, then immediately reads `#ngStats .stat-card` count. On slower runs the read happens before stats hydrate → 0 cards → assert fail. Other Analytics tabs in the same file already use `page.waitForFunction(...)` to poll for content (e.g. Distance tab on line 654). Neighbor Graph block was missing the equivalent wait. ## Fix Add the same defensive wait before counting: ```js await page.waitForFunction( () => document.querySelectorAll('#ngStats .stat-card').length >= 3, { timeout: 8000 }, ); ``` Test-only change. No frontend code touched. Bounded by 8s timeout matching other Analytics waits. Co-authored-by: Kpa-clawbot <bot@example.invalid> |
||
|
|
abd9c46aa7 |
fix: side-panel Details button opens full-screen on desktop (#892)
## Symptom 🔍 Details button in the nodes side panel does nothing on click. ## Root cause (4th regression of the same shape) - Row click → `selectNode()` → `history.replaceState(null, '', '#/nodes/' + pk)` - Details button click → `location.hash = '#/nodes/' + pk` - Hash is already that value → assignment is a no-op → no `hashchange` event → no router → panel stays open. ## Fix Mirror the analytics-link branch already inside the panel click handler: `destroy()` then `init(appEl, pubkey)` directly (which hits the `directNode` full-screen branch unconditionally). Also `replaceState` to keep the URL in sync. ## Test New Playwright E2E: open side panel via row click, click Details, assert `.node-fullscreen` appears. ## Why this keeps regressing Every time we tighten the row-click handler to use `replaceState` (correct — avoids hashchange flicker), the button-click handler that uses `location.hash` becomes a no-op for the same pubkey. Need to remember they're coupled. Worth a follow-up to extract a `navigateToNode(pk)` helper that always works regardless of current hash state — filing as #890-followup if not already there. 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> |
||
|
|
67aa47175f |
fix: path pill and byte breakdown agree on hop count (#885)
## Problem On the packet detail pane, the **path pill** (top) and the **byte breakdown** (bottom) showed different numbers of hops for the same packet. Example: `46cf35504a21ef0d` rendered as `1 hop` badge followed by 8 node names in the path pill, while the byte breakdown listed only 1 hop row. ## Root cause Mixed data sources: - Path-pill badge used `(raw_hex path_len) & 0x3F` (= firmware truth for one observer = 1) - Path-pill names used `path_json.length` (= server-aggregated longest path across observers = 8) - Byte breakdown section header used `(raw_hex path_len) & 0x3F` (= 1) - Byte breakdown rows were sliced from `raw_hex` (= 1 row) - `renderPath(pathHops, ...)` iterated all `path_json` entries For group-header view, `packet.path_json` is aggregated across observers and therefore longer than the raw_hex of any single observer's packet. ## Fix Both surfaces now render from `pathHops` (= effective observation's `path_json`). The raw_hex vs path_json mismatch is still logged as a console.warn for diagnostics, but does not drive the UI. With per-observation `raw_hex` (#882) shipped, clicking an observation row already swaps the effective packet so both surfaces stay consistent. ## Testing - Adds E2E regression `Packet detail path pill and byte breakdown agree on hop count` that asserts: 1. `pill badge count == byte breakdown section count` 2. `rendered hop names ≈ badge count` (within 1 for separators) 3. `byte breakdown rendered rows == section count` - Manually reproduced on staging with `46cf35504a21ef0d` (8-name path + `1 hop` badge before fix). Related: #881 #882 #866 --------- Co-authored-by: you <you@example.com> |
||
|
|
a605518d6d |
fix(#881): per-observation raw_hex — each observer sees different bytes on air (#882)
## Problem Each MeshCore observer receives a physically distinct over-the-air byte sequence for the same transmission (different path bytes, flags/hops remaining). The `observations` table stored only `path_json` per observer — all observations pointed at one `transmissions.raw_hex`. This prevented the hex pane from updating when switching observations in the packet detail view. ## Changes | Layer | Change | |-------|--------| | **Schema** | `ALTER TABLE observations ADD COLUMN raw_hex TEXT` (nullable). Migration: `observations_raw_hex_v1` | | **Ingestor** | `stmtInsertObservation` now stores per-observer `raw_hex` from MQTT payload | | **View** | `packets_v` uses `COALESCE(o.raw_hex, t.raw_hex)` — backward compatible with NULL historical rows | | **Server** | `enrichObs` prefers `obs.RawHex` when non-empty, falls back to `tx.RawHex` | | **Frontend** | No changes — `effectivePkt.raw_hex` already flows through `renderDetail` | ## Tests - **Ingestor**: `TestPerObservationRawHex` — two MQTT packets for same hash from different observers → both stored with distinct raw_hex - **Server**: `TestPerObservationRawHexEnrich` — enrichObs returns per-obs raw_hex when present, tx fallback when NULL - **E2E**: Playwright assertion in `test-e2e-playwright.js` for hex pane update on observation switch E2E assertion added: `test-e2e-playwright.js:1794` ## Scope - Historical observations: raw_hex stays NULL, UI falls back to transmission raw_hex silently - No backfill, no path_json reconstruction, no frontend changes Closes #881 --------- Co-authored-by: you <you@example.com> |
||
|
|
0ca559e348 |
fix(#866): per-observation children in expanded packet groups (#880)
## Problem
When a packet group is expanded in the Packets table, clicking any child
row pointed the side pane at the same aggregate packet — not the clicked
observation. URL would flip between `?obs=<packet_id>` values instead of
real observation ids.
## Root cause
The expand fetch used `/api/packets?hash=X&limit=20`, which returns ONE
aggregate row keyed by packet.id. Every child therefore carried
`data-value=<packet.id>`.
## Fix
Switch the expand fetch to `/api/packets/<hash>`, which includes the
full `observations[]` array. Build `_children` as `{...pkt, ...obs}` so
each child row gets a unique observation id and observation-level fields
(observer, path, timestamp, snr/rssi) override the aggregate.
## Verified live on staging
Tested on multiple packets:
- Click group-header → side pane shows observation 1 of N (first
observer)
- Click child row → pane updates to show THAT observer's details:
observer name, path, timestamp, obs counter (K of N), URL
`?obs=<observation_id>`
## Tests
592 frontend tests pass (no new ones — this is a wiring fix, live
E2E-verified instead).
Closes #866
---------
Co-authored-by: Kpa-clawbot <agent@corescope.local>
Co-authored-by: you <you@example.com>
|
||
|
|
bb09123f34 |
test(#833): update deep-link Playwright assertion for full-screen desktop view (#834)
Closes #833 ## What Update Playwright E2E assertion for desktop deep link to `/#/nodes/{pubkey}`. Now expects `.node-fullscreen` to be present (matches the spec set by PR #824 / issue #823). ## Why The previous assertion encoded the old pre-#823 behavior — "split panel on desktop deep link." PR #824 intentionally removed the `window.innerWidth <= 640` gate so desktop deep links open the full-screen view (matching the Details link path that #779/#785/#824 ultimately made work). The test failed on every PR that rebased onto master, blocking `Deploy Staging`. ## Verified - 1-test diff, no other behavior change - Mobile-viewport assertions elsewhere already exercise the same `.node-fullscreen` selector Co-authored-by: Kpa-clawbot <agent@corescope.local> |