mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 04:40:05 +00:00
4e91c5cf0bb2e35e12a074580f4d993c32cb6f00
274
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
352a0ccd19 |
feat(region-filter): configurable region quick picks (#2170)
## Summary **Independent of every other open PR.** Choosing a pick saves the selection, redraws the control and notifies the page's listeners itself, exactly as a click in the control does, so it re-queries straight away whether or not #2168 (`setSelected` notifies) is merged; with both in, nothing fires twice. Region quick picks are named groups of region (IATA) codes, offered as one-tap choices inside the region filter, e.g. "Northern California" for a dozen airport codes instead of ticking each box. One deployment has been injecting these into the page through an nginx `sub_filter` and a script that wraps `window.RegionFilter`; this makes them a configured feature. ```jsonc "regionQuickPicks": [ { "name": "Northern California", "description": "Bay Area and Sacramento observers.", "regions": ["SJC", "SFO", "OAK", "SMF"] } ] ``` - **Server:** `GET /api/config/region-quick-picks` returns the groups cleaned up: names and codes trimmed, codes upper-cased and de-duplicated in their configured order, groups without a name or codes dropped, at most 20 groups of 200 codes, always a list. `/api/config/regions` is unchanged. - **Region filter:** loads the groups with the regions and offers every group with at least one observer today, inside the region control: - dropdown layout: rows of the same kind as **All** (checkbox and bold name), directly under All with a divider before the single regions; the row is ticked while that pick is the selection, ticking it selects the pick and closes the menu, unticking goes back to all; - pill layout: the picks lead the same bar, set off from the single regions by a thin divider. - A tap selects the group's codes that have an observer and re-queries straight away. - The selector then reads the group's name ("South Bay ▾") instead of "2 Regions", and the button shows as pressed. Selecting the same codes by hand does the same. - Tapping the active group again goes back to all regions. - A failed fetch, or no configured groups, leaves the filter exactly as before. - **Mobile:** the rows take the menu's 44 px row height under `(pointer: coarse)`; the pill-bar picks reuse `.region-pill` and its touch target. Inside `.filter-bar` (Packets) the existing touch rule for inputs made every region checkbox a 44 px square; the checkbox is now 20 px and the 44 px row is the tap target. - **Semantics:** like every region filter, a group means "heard by observers in these regions", not "located there"; `config.example.json` says so. No existing issue covers this. ## Screenshot Nodes page with two picks configured (example config below), after tapping **South Bay**: the pick and its codes (SJC, MRY) are selected, and the list re-queried for them. Taken against `test-fixtures/e2e-fixture.db`.  ## Config example ```json "regionQuickPicks": [ { "name": "South Bay", "description": "San Jose and Monterey observers", "regions": ["SJC", "MRY"] }, { "name": "East and North Bay", "description": "Oakland and San Francisco observers", "regions": ["OAK", "SFO"] } ] ``` Each pick needs a `name` and at least one code; `description` is optional (shown as the tooltip). Codes are trimmed, upper-cased and de-duplicated; a pick with no code that has an observer today is not shown. At most 20 picks of 200 codes. ## Browser validation The branch's server was run locally against a migrated copy of `test-fixtures/e2e-fixture.db` (observers in MRY, OAK, SFO, SJC) with three groups configured: "South Bay" (SJC, MRY, SCZ), "East and North Bay" (OAK, SFO, sts) and "Nowhere yet" (XXX). Checked in headless Chromium: | check | desktop 1280 px | phone 390 px (touch) | |---|---|---| | groups shown | South Bay, East and North Bay ("Nowhere yet" hidden) | same | | tap South Bay selects | SJC, MRY (SCZ has no observer) | same | | next `/api/packets` request | `region=SJC,MRY` | `region=SJC,MRY` | | selector label | "South Bay ▾" | "South Bay ▾" | | second tap | back to all regions | same | | button height | 24 px | 44 px | | page wider than viewport | no | no | | page errors | none | none | Checked on Packets (dropdown layout) and Nodes (pill layout). Three layout problems found this way on the phone were fixed before opening: a sideways-scrolling row clipped at the screen edge, the selector squeezed into a narrow column (the filter group is a non-wrapping flex row), and the two wrapped rows spread apart on Nodes (`align-content`). **Revision 1:** the first version put the picks in a separate row beside the selector; they moved inside the dropdown. Re-checked with a 14-region list and three picks on Packets: desktop 1280 px dark and phone 390 px light both keep the menu on screen (menu 340 px wide, page not wider than the viewport), the pick closes the menu and the trigger names the pick. **Revision 2:** picks are checkbox rows like **All** instead of chips. Re-checked on Packets at 1280 px dark and 390 px light: rows render bold under All, the divider separates the single regions, ticking a pick ticks its codes and names the trigger, no sideways scroll. Unrelated and not changed: on Nodes at 390 px the region row (existing pills included) starts flush with the screen edge. ## Tests - Red commit `81ea67d6`: the config type, a stub normaliser and endpoint returning nothing, and tests. - Go: cleaning (trim, upper-case, de-duplicate, drop incomplete groups), caps, nil config, and the endpoint's JSON. - Frontend, `tests/e2e/test-region-quick-picks.js` (moved from `tests/unit/` in `fced69a7`; it needs jsdom, so it runs in the E2E job like `test-table-sort.js`): the real `region-filter.js` in **jsdom** against faked config endpoints. Nine cases: rendering and hiding groups with no observer, selecting and re-querying, the selector label, toggling back, a hand-made selection marked active, the pill layout, no groups, a failed fetch, HTML escaping. Seven fail on the stub, all on assertions. - Green commit `0e33210e`: the implementation, styles, `config.example.json` (`regionQuickPicks` + `_comment_regionQuickPicks`), `docs/api-spec.md`, and the OpenAPI route list (`TestOpenAPICompleteness` requires it). The jsdom test also had a cross-realm comparison bug fixed in this commit (`Array.from` on arrays coming out of the jsdom window). - `make test`: 16 of 16 modules pass. Frontend suites: 195 of 196; the one failure, `test-issue-1956-release-routing.js`, needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally. - Red commit `f991060d` (revision): picks live inside the dropdown menu before the checkboxes, choosing one closes the menu, and in the pill layout they share the region bar; two fail on assertions against the old layout. Green commit `1f720e35`. Frontend 195 of 196 (same environment-only failure). - Red `134f5256` / green `da0adb50` (second revision): picks are checkbox rows between All and the single regions with a divider, no chips in the menu; ticking and unticking a pick row. The green commit also repairs `style.css`: `1f720e35` had prepended its block to the top of the file (its edit matched an earlier `.region-pill {`) and left the previous rules in place. The file now differs from `master` by 13 lines. ## Review notes - Groups are offered only when at least one of their codes appears in `/api/config/regions`, and only those codes are selected. The stored selection is pruned to known codes on load anyway, so selecting absent codes would not survive a reload. - Customizer (AGENTS.md rule 8): groups are operator configuration; a customizer editor could come later. ## SonarLint SonarQube Community rules (the SonarLint engine) were run on the files this PR touches, and only issues on lines it adds are listed. - **Fixed** in `b84ace4e`: - `normalizeQuickPickCodes` split out of the config normaliser (S3776, 17 > 15); - a comment on why a failed quick-pick fetch is ignored (S2486); - `for…of` in `activePick` (S4138); - in the tests: `node:` specifiers, `includes()`, and an explicit string comparator for sorted selections (S7772, S7765, S2871). - **Kept on purpose:** - S2486 is still reported on that `catch`. Swallowing the error is the intended behaviour: the filter works as before, just without picks. - S1523 on the `vm` test loader, as above. ## Checked against the repo rules - Rebased onto `master` (`673442cc`). Each red commit was re-run and still fails on its assertions; each green commit passes (TDD.md). - `make test`: all 16 modules pass. Frontend suites: all pass except `test-issue-1956-release-routing.js`, which needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally (#2160 addresses it). - No private data: test fixtures and examples use the codes the existing tests use (SJC/SFO/LAX, `#be`-style scopes). - No new `map[string]interface{}`, no hard-coded colours. `git add` was explicit, and there is one logical change per commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
29095658e9 |
fix(nodes): explain packet and observation counts (#2132)
Red commit: `d8a6d42` ([CI assertion](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37695889461)). Review-regression red: `f73643b` ([CI assertion](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37703995494)). Fixes #2131. Partial fix for #1154: U2 only; other audit items remain open. Both node views explain distinct transmissions versus recorded observations. The shared help is reachable by keyboard and touch, stays readable at either scroll edge, and remains open when the pointer enters its text. Escape dismisses help while preserving node context and focus; a second Escape keeps the existing navigation behavior. Count expressions and API requests are preserved. One controller per nodes page handles one active help element and one queued animation frame. Scroll/resize work is coalesced; listeners are removed on page teardown. Work is constant relative to packet/node counts, with no dataset scans or new settings/dependencies. E2E assertion added: `tests/e2e/test-e2e-playwright.js:114`. Thirteen cases cover exact counts, accessible description, keyboard/touch, hover transfer, Escape, and viewport/scroll bounds. Existing metric assertions are unchanged. Browser verified: http://localhost:63508 using Chromium, controlled fixtures and actual API data. Desktop/mobile screenshots retained locally; additional checks exercised resizing and repeated page cleanup. Local validation: 189 standalone suites; core browser 148 passed, 3 fixture-dependent skips (existing manual skips unchanged); location/metric suite 6 passed; syntax, theme variables and lint passed (0 errors, 92 existing warnings). ## Preflight overrides - OpenClaw preflight/profile unavailable; repository checks and local Chromium used. - Windows ingestor symlink fixture lacks permission. Backend files are unchanged; full Linux Go CI is required. |
||
|
|
abbe6846ff |
feat: propose and approve hashtag channels (user management part D) (#2139)
Part D of #2128: logged-in users propose a hashtag channel, an admin approves, rejects or later revokes it, and the ingestor decrypts approved channels without a restart. It is the first consumer of a generic proposal table, and answers the request in #2092. Stacked on #2138 (part C). Review the commits after that branch; GitHub shows C's commits here until it is merged. ## The situation - An instance decrypts only the hashtag channels listed in `hashChannels`. Users who want a city or club channel shown have to ask the operator, who edits `config.json` and restarts the ingestor. - #2092 asked for a suggest-and-approve flow; a downstream fork (dborup/CoreScope#99) runs one with anonymous suggestions and a file queue. ## What this PR adds **Name rules (`internal/channel`).** `ValidateHashtagName`, mirrored in the browser: - at most 31 UTF-8 bytes including `#` (firmware `ChannelDetails.name[32]`), case preserved, `Public` refused in any case; - control, bidi, separator and format characters refused except ZWJ, plus invisible fillers (`Other_Default_Ignorable_Code_Point`, U+2800, non-ASCII spaces). Go and JS give the same verdict for every code point. **Store.** Schema v4, one generic `proposals` table (kind, subject, status pending/approved/rejected/revoked, proposer, reviewer, note). One row per (kind, subject); limits are checked in the same transaction as the change. **Server** (only with `userManagement.channelProposals.enabled`) - `POST /api/proposals`, `GET /api/account/proposals`, `GET /api/admin/proposals`, `POST /api/admin/proposals/{id}/approve|reject|revoke`. - 400 for an invalid name. 409 for a duplicate, a rejected name, or a name already in `hashChannels`/`channelKeys` ("this channel is already decrypted on this instance"). 429 over a limit (`maxPending` 100, `maxApproved` 128, `perUserPerDay` 5). - `GET /api/channels` gains `approvedChannels`, so approved channels are listed before they have traffic. With the feature off the response is byte-identical. **Ingestor.** Opens `users.db` with `mode=ro` every 60 seconds, re-validates the names, and swaps its key map through an `atomic.Pointer`. Configured channels always win, and a failed read keeps the last good set. The ingestor never writes `users.db`; AGENTS.md now says so. **Frontend.** "Propose for everyone" in the add-channel dialog, an "approved" marker in the channel list, "My proposals" on the account page, and an admin "Proposals" tab with a warning on approve. The packet detail pane now escapes the channel name: until now only the operator chose those names. Spec: [`docs/specs/2026-10-07-channel-proposals-design.md`](https://github.com/efiten/CoreScope/blob/feat/channel-proposals/docs/specs/2026-10-07-channel-proposals-design.md). ## Performance Every key is tried on each GRP_TXT that no key opens (`decodeGrpTxt`). Benchmark of that worst case: | Keys | Time per packet | |---|---| | 320 configured | 204 to 216 µs | | 320 configured + 128 approved | 272 to 320 µs | That is about +33%, linear in the key count and capped by `maxApproved`. With the feature off the decoder gets the identical map and no ticker runs. ## Verification - `internal/channel`, `internal/users` (with `-race`), `cmd/server` and `cmd/ingestor` pass locally, 53 new Go tests. The ingestor's `TestWriteStatsAtomic_SymlinkAtDestIsReplaced` needs symlink rights on Windows and fails on `master` too. - `sh test-all.sh` exits 0; the XSS gate in diff mode passes. - User-management E2E: 21 of 21 steps locally (propose, approve, the channel appears, revoke removes it). - On our staging and production instance since 7 October 2026. ## Not in this PR - Private (PSK) channels: their keys stay in the browser (#725). - Anonymous proposals. - Names that need ZWNJ (some Persian and Urdu spellings) and subdivision flags are refused, because ZWNJ and tag characters are format characters. Allowing them later only widens the rule, so no approved name gets stranded. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
18a634c115 |
feat: admin dashboard with overview and audit log (user management part C) (#2138)
Part C of #2128: an admin area with an overview, the user table and a global audit log. Off by default like the rest of user management: with `userManagement` off nothing changes, and non-admins get no new UI. This is the first of 4 stacked PRs (C, D, E, then a small export/backup follow-up). Each later one contains this branch; review them in order. ## The situation - An admin could manage users one by one in `#/admin/users`, but nothing showed whether the instance needs attention: accounts stuck in activation, bouncing mail, someone guessing a password, an MQTT source that dropped. - The audit log existed (`internal/users/audit.go`) but could only be read per user, and logins were not recorded. ## What this PR adds **Store (`internal/users`)** - Schema v3: an index on `audit_log(at)`. - `AuditList` with filters (action or group prefix like `user.login.*`, user as actor or target, period) and keyset pagination; `PruneAudit`; `Stats` for the user figures. **Server** - `GET /api/admin/stats` (typed struct): accounts by status, admins, registrations and active users over 7 and 30 days, logins and failed logins in 24 hours, mail by final status, and the attention items computed server-side. - `GET /api/admin/audit`: filtered, newest first, `next` cursor. - `GET /api/admin/users` gains `bouncing=1`. - Logins are audited as `user.login` and `user.login.failed` (reason `wrong_password`, `pending` or `disabled`). An unknown address writes no row. The writes are asynchronous, so the login response does not wait on `users.db`. Login rows are pruned after 90 days. **Frontend** - `#/admin?tab=overview|users|audit`: `admin.js` (tab shell), `admin-overview.js` ("Needs attention", Users card, System card from the existing health, MQTT and observer endpoints), `admin-audit.js` (filters in the URL, "Load more"). `admin-users.js` becomes the Users tab; the old `#/admin/users` link rewrites to it. - `/api/healthz` is read on open and on Refresh only, not on the 60-second timer, because it walks every packet under a read lock. Spec: [`docs/specs/2026-10-07-admin-dashboard-design.md`](https://github.com/efiten/CoreScope/blob/feat/admin-dashboard/docs/specs/2026-10-07-admin-dashboard-design.md). ## Verification - `internal/users` and `cmd/server`: `go vet` and `go test` pass locally, 26 new Go tests. One upstream test, `TestSaveGeoFilterPreservesFileMode`, also fails on Windows on plain `master` (file modes) and is unrelated. - `sh test-all.sh` exits 0; the XSS gate in diff mode passes. - User-management E2E with the `e2etest` build: 13 of 13 steps locally. - Running on our staging and production instance since 7 October 2026. ## Not in this PR - Restricting existing pages (perf, MQTT status) to logged-in users or admins. The admin area adds no access mechanism of its own, so that stays a later router rule plus endpoint check. - Server-enforced customizer tab restrictions (#1508). - The 24-hour, 5-attempt and 10-minute attention thresholds are constants for now (AGENTS.md rule 8: customizer later). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d232072f85 |
feat: optional user accounts (part A: foundation) (#2129)
Part A of #2128: optional, off-by-default user accounts. With the feature off nothing changes; with it on, visitors can register and log in, and admins manage users and use the operator actions without the API key. PR #2130 (settings sync) builds on this one. The two are meant to be merged together. ## The situation - Operator actions (geofilter save and prune, backup, perf reset) need the shared `apiKey`. There is no per-person right. - Nothing in CoreScope knows who a visitor is, so the requests in #2128 that need that (#1835, #2092, #1508, #730) have nothing to build on. ## What this PR adds **Two new Go modules** - `internal/users`: a separate `users.db` (SQLite through `modernc.org/sqlite`) with users, sessions, single-use tokens, an audit log and a mail log. Passwords use argon2id. - `internal/mailer`: a `Mailer` interface with a Brevo client (send, delivery events, webhook parsing) and an in-memory fake for tests. **Server (`cmd/server`)**, active only with `userManagement.enabled` - 24 routes, all documented in OpenAPI under the `users` tag ([`auth_routes.go`](https://github.com/efiten/CoreScope/blob/feat/user-management/cmd/server/auth_routes.go)): - auth: register, activate, login, logout, me, forgot, reset; - account: profile, password, email change with confirmation, sessions, self-delete; - admin: list, detail, disable, enable, delete, role, resend activation, manual activation, mail status refresh; - a Brevo webhook, registered only when `mail.webhookSecret` is set. - `requireAdmin` replaces `requireAPIKey` at the 7 operator call sites: the API key **or** an admin session. With the feature off it is the old API-key gate (`TestRequireAdminWithoutUserManagementIsAPIKeyGate`). - `/api/config/client` gets `userManagement: {enabled: true}` only when the service started; with the feature off the response is byte-identical. **Frontend** - `auth.js` (header account control, request helper that adds the CSRF header), `account.js` (login, register, activate, forgot, reset, confirm email, my account), `admin-users.js` (`#/admin/users`, deep-linked filters), `account.css` (theme tokens only). - On phones the top-bar control is hidden, so a conditional entry goes into the bottom-nav "More" sheet and the nav drawer. - The customizer geofilter tab and the Perf "Reset stats" button use the admin session when there is one. **Config.** A `userManagement` block (`config.example.json`, [`docs/user-guide/accounts.md`](https://github.com/efiten/CoreScope/blob/feat/user-management/docs/user-guide/accounts.md)). The Brevo key can come from `CORESCOPE_BREVO_API_KEY`. The server refuses to start when the block is enabled but incomplete. ## Security choices - Session cookie `cs_session`: HttpOnly, SameSite=Lax, Secure when `publicBaseUrl` is https. Every cookie-authenticated state change needs the `X-CS-CSRF` header and a matching Origin. - Activation needs the token **and** the account password. Without the password, an attacker who keeps re-registering a known address could get the owner to activate an account that carries the attacker's password. - Register, forgot and email change answer identically for known and unknown addresses. A password reset ends all sessions, a password change ends all other sessions, and both end outstanding email-change links. - Rate limits: login 10 per 15 minutes, register and forgot 5 per hour, per IP and per address. The bucket count is capped. `trustedProxies` makes the per-IP limits see real client IPs behind a proxy. - Server logs carry `#<user id>`, never addresses, tokens or passwords; mail-provider error texts are redacted before logging. ## Performance No change to an existing hot path with the feature off. With it on: - One `users.db` lookup per authenticated request (session by token hash). - The admin user table rebuilds its `tbody` on each filter change. `users.List` caps the result at 1000 rows (`internal/users/users.go`), which bounds the rebuild. - `map[string]interface{}` in `openapi.go`: 79 before, 78 after. ## Verification - `internal/users`, `internal/mailer` and `cmd/server`: `go vet` and `go test -race` pass locally. 121 new Go tests. - `cmd/server` with `-tags e2etest`: vet and the e2e hook tests pass. - `sh test-all.sh` exits 0. `tests/unit/test-user-management-ui.js`: 67 passing (vm, real modules). - `tests/e2e/test-user-management-e2e.js` (6 steps) passed locally against an `e2etest` build with the fake mailer and against a feature-off build. CI builds the `e2etest` binary and runs the suite on a second server (`deploy.yml`). - On a staging instance with a real Brevo key: register, activation mail delivered, activate, admin table, "Refresh status" showing sent, deferred, delivered, opened and clicked. ## Not in this PR - Settings sync (#2130), the admin dashboard, approval flows and notifications (parts B to E of #2128). - A `requireReadAuth` mode (#1835). Sessions from this PR are what such a mode would accept. - Binary size and build time with `modernc.org/sqlite` linked next to `mattn/go-sqlite3` were not measured. Their driver names do not collide. #1992 discusses the driver choice. - No Brevo webhook was configured on staging; delivery status there came from "Refresh status". --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
9e49db9725 |
feat(nodes): group adverts by observed route evidence (#2085)
Red commit: `ac60248` ([two intended label failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37137429333)). Original grouping red: `767da9b` ([CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/36353796690)). Fixes #2073. #2088 is merged. Rebased onto master `415362c`; all eight patches are unchanged, including the test-first history. Recent Adverts groups the bounded sample by authoritative `advert_kind`: Flood, Mixed flood / direct (empty path), Direct (empty path), and Other / unknown. Missing evidence stays unknown. Each advert appears once, preserving reception metadata, order, packet links, counts and origin/history explanations. Wording describes observed remaining paths without inferring original send mode or RF distance. Validation at `6317baf`: - All 185 standalone suites passed; syntax, XSS and lint passed (zero errors, 92 existing warnings). - Browser verified: `http://127.0.0.1:51827` (temporary Go fixture server, now stopped), desktop pane/full view and mobile. Mixed evidence, exact links and caveats checked again after pushing. - The expanded fixture exposes the existing #2104 failure in the unmodified core harness. Supplemental integration with #2105's exact test correction passes 135 checks, with three existing skips. Both PRs remain separate. - [Fresh green CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37138366726) passed; three independent automated reviews found no must-fix issues. E2E assertion added: `tests/e2e/test-e2e-playwright.js:142`. Grouping is O(n) over the existing 20-advert sample. No new requests, settings, colors or dependencies. ## Preflight overrides External OpenClaw runner/profile unavailable; repository checks and actual local Chromium were used. |
||
|
|
415362c5af |
fix(packets): resolve path hops with the observer that heard them (#2097) (#2099)
Fixes #2097. Every 1-byte path prefix on this network is shared. Measured on the live deployment: 254 prefixes cover all 2043 nodes, nine of them on a single prefix, and not one node has a prefix to itself. The packets page printed one of those nine as a certainty. It could already have said otherwise. `hop-resolver.js` marks such a hop `ambiguous` and returns its full candidate list; `hop-display.js` renders a warning badge with a count and a candidate popover; `renderHop(h, observerId)` reads a per-observer cache key. None of it fired, because `HopResolver.resolve()` takes six parameters and `packets.js` passed one. Without `observerId`, `packetIata` is null, `nodeInRegion()` never runs, no candidate is flagged `regional`, `globalFallback` stays false, and `hop-display.js` computes a badge count of 0. The whole chain stayed silent while the data said it was a guess. ## What this changes **Resolution carries the observer.** `resolveHops()` takes it and writes the per-observer cache key that `renderHop()` has always read and nothing ever wrote. `resolveHopsForPackets()` groups a multi-packet call by observer so each group is filtered on its own. One site was the bug in miniature: it wrote its result under `hopNameCache[k + ':' + pkt.observer_id]` after resolving without that observer, so the key promised something the value was not. **The observer's position is the anchor.** `resolve()` has always accepted `observerLat`/`observerLon` as the anchor at the receiving end: when nothing later in the path is resolved, the observer is the next known position, which is what `pickByAffinity` needs. `observerPosition()` is the single lookup, used by both resolve paths. This matters more than the IATA route, which turns out to be dead on this network. `nodeInRegion()` looks the observer's code up in `/api/iata-coords`; **0 of 42 observers have their code in that table** (`ANR BRU GNE HEP KJK LGG MST NRW OBL OST` are all missing, while its 54 entries are `AMS`, `APC`, `ATL` and the like). So the 300 km filter has never excluded anything here. 27 of 42 observers report lat/lon directly, and that works. Filed separately as #2098, since it is a network-wide lookup that resolves nothing and hop resolution may not be its only consumer. **Each badge belongs to its pill.** The badge is a sibling after the pill, so a path read `A [8] → B [6] → C` with nothing to say which name the 8 qualified. Pill and badge now render inside one `white-space: nowrap` `.hop-group`, leaving the separator outside the pair. Only a hop that carries a badge is wrapped; wrapping every hop would change the layout of every path for nothing. That markup predates this work, but these commits are what made it visible — before them the badge essentially never appeared on this page. **The list summarises, the detail pane does not.** Every 1-byte hop getting its own badge turned a table row into a line of warning triangles. `renderPath()` takes `{ summary: true }` for the three list call sites: names with no per-hop badge, and one indicator reading "N of M hops have more than one candidate". The two detail-pane sites are unchanged and keep a badge per hop, because that is where the candidates actually get read. `HopDisplay` gains `opts.badge === false` for it, and the hop keeps its `hop-ambiguous` class either way so CSS can still mark it. ## Scope Deliberately not included: chaining the next resolved hop to narrow a candidate set to one. `pickByAffinity` already scores on graph edges and would do it; it needs neighbouring context this call does not yet supply, and that belongs in its own change to `hop-resolver.js`. What is here makes the uncertainty visible and picks a defensible candidate rather than the first in index order. ## Verification 30 unit tests in `tests/unit/test-issue-2097-hop-ambiguity-badge.js`, registered in `test-all.sh`. Full frontend suite exits 0. Four of them are structural guards over `packets.js`, and each was mutation-checked by reverting the change it protects: - no `HopResolver.resolve()` call passes the hops alone - no call passes an observer id and drops its position - one helper does the observer lookup - the detail pane calls `renderPath` without `summary` The behavioural test for the anchor runs with `iataCoords` deliberately empty, which is the live state, and lists the distant candidate first: without an anchor the resolver keeps candidate order and picks it, with one it does not, and the hop stays reported as ambiguous with all candidates listed. **Browser validated** on a staging deployment of this branch, against live data: | | list | detail pane | |---|---|---| | per-hop badges | 0 | 24 | | path indicators | 1 | 0 | | badges outside a `.hop-group` | — | 0 | On the packet that prompted the issue, the first hop now resolves to a repeater 10.6 km from the transmitter instead of one 126 km away, and the second hop resolves to the only candidate that has a recorded neighbour edge matching the next hop in the path. ## Note for reviewers The last commit exists because staging disagreed with the tests. After the anchor landed, the detail pane still picked the distant candidate: it resolves through its own `HopResolver.resolve` call, which had the observer id but a null position. The guard that now fails on exactly that shape is what would have caught it before the deploy. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b9fb3d244c |
feat(analytics): split adverts using recorded route evidence (#2088)
Red commits: `dc7dcbe`, `76e3217` ([assertion failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37059977385)). Fixes #2041; supplies #2085's backend contract. Records at most two route bits per retained advert. Node flood counts now use the same evidence, including mixed and transport-flood adverts. Analytics failures log/count errors while core observation, path and liveness writes continue. The unrelated recent-advert limit clamp is removed. Direct adverts (empty path) describes the observed remaining path across valid hash widths. Firmware can forward an advert down to an empty path; this cannot establish original send mode or RF distance. The `zero_hop` API spelling remains compatible. Successful post-upgrade evidence writes survive observation replacement and restart. Older overwritten frames and failed writes can leave gaps. The UI explains automatic background backfill. ## Measured validation Synthetic Windows workloads; timings are not production guarantees: - 300K retained adverts, one-hour window: median 64.35→39.84 ms; 13.98 MB→145 KB allocated. Seven-day allocations unchanged; five paired samples. - 1M transmissions/11M observations: backfill 5m43s. Simple 10 Hz writer: all 3,423 observations persisted; p99 28 ms, maximum 323 ms. - Simulated pre-backfill cursor: retained evidence ready 4m21s; full catch-up 6m41s, 122 cache invalidations. Normal completed-migration startup bulk-loads current evidence. Portable opt-in scale tests and window benchmarks are included. All 183 frontend suites, full server tests, targeted races, lint and real-API desktop/mobile checks passed. Full [Linux CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/37063210229) passed at `36def9c`; three independent reviews are clear. Local Windows testing hit the unchanged symlink-privilege test. No configuration changes. Preflight overrides: external script/OpenClaw profile unavailable; repository checks and Chromium used. |
||
|
|
a4a447acea |
fix(packets): pack short columns, one expand arrow, Full Names toggle (#2090)
The packets table wastes most of its width on short columns, draws two arrows on grouped rows, and cuts every observer and hop name short. This fixes all three and adds a way to read the full path when it does not fit. ## Why the columns were wide `makeColumnsResizable` measured each column once on load and stored the widths as **percentages** of the table. Percentages grow with the screen: on a 2000px display "17s ago" sat in a 180px column. The measurement also read the virtual-scroll spacer row — a single `colspan` cell as wide as the table — as column 0, so the expand column came out 160px wide locally and about 950px on a live instance with grouped rows. Measured on the fixture at 2000px: | Column | Before | After | |---|---|---| | Expand | 162–230px | 32px | | Time | 111px | 67px | | Size | 64px | 46px | | HB | 44px | 29px | | Scope | 80px | 48px | | Path / Details | ~180px each | ~650px each | ## The change **Column packing.** A new `fitColumnsToContent` (`app.js`) sizes every column except Path and Details to its widest cell, in px; those two split what is left. Every visible column gets an explicit width and the table gets their sum: a spacer row keeps a slot for each hidden column, and leaving any column `auto` hands those slots a share of the slack as a blank strip at the right edge. When space is short (detail panel open, narrow window) the widest fixed columns give way first, then Path and Details shrink to 60px, and only then does the wrapper scroll. The split itself is a pure function, `distributeColumnWidths`, with unit tests. It re-lays out when the container resizes (detail panel), when TableResponsive hides or reveals columns (a new `table-columns-changed` event), and after sorting or a column toggle. After each render `grow()` widens columns for the rows just inserted and never narrows them, so nothing jitters while scrolling. Drag handles store px; double-clicking one resets it. Path and Details have no handle. `makeColumnsResizable`, still used by the nodes, observers and analytics tables, now skips `colspan` rows when measuring; other rows are measured as before. **One expand arrow.** A CSS `::before` "▶" sat next to the SVG caret, which pointed up when collapsed. The glyph is gone and the caret points right, then down. **Full Names.** A toolbar toggle next to Hex Paths shows observer and hop names untruncated. It is saved per browser; a link can carry `fullNames=1`, which applies to that page without overwriting the visitor's saved preference. Paths stay on one line: the virtual scroller positions rows at one fixed height, so wrapping would make scrolling jump. **The "+N" path pill, and a full-path popover.** Hops past the column edge were meant to go behind a "+N" pill (#1124), but the pill was appended after the overflowing chips and so sat past the clipped edge: it never showed. It is now sticky at the right edge, opaque on hover, and counts hop chips only (it used to count warning icons too). Hovering or focusing it shows the full path, one hop per line and numbered; click, Enter or Space pins it until an outside click or Escape, and it closes when the page changes or its row scrolls away. Clicking the pill no longer selects the row underneath. **Two related fixes.** - Rows drawn before `/api/observers` resolves show raw 64-char pubkeys. `loadObservers` now re-renders and re-measures, instead of leaving Observer at the pubkey's width (~600px under Full Names). - Group child rows indented every cell by 20px, which misaligned them with the headers and widened every column on expand. Only the Time cell is indented now. Anyone who had dragged packet columns gets the new defaults once: the old percentage key is dropped. ## Performance `grow()` runs after every `renderVisibleRows`, so it is on the hot path. Measured in Chromium on the fixture, 300 iterations, three runs: | Case | Cost | |---|---| | Full render, 63 rows measured | +0.88–0.93ms over the forced reflow alone | | Scroll step (incremental render) | ~73 `Range` rect reads, only the inserted rows | | Large jump / re-render | ~569 rect reads | Before narrowing `grow()` to inserted rows, one scroll step with 92 rendered rows made 5,472 rect reads. The DOM row count is bounded by the virtual-scroll window, so the cost does not grow with the packet count. `distributeColumnWidths` is a binary search over at most ~10 columns. ## Tests - `tests/e2e/test-packets-compact-columns-e2e.js`, 16 cases: short columns stay under fixed px bounds at 1920px; Path and Details take the freed width; hiding both still fills the wrapper; no blank strip and no horizontal scroll, with and without the detail panel, at 1920, 1100, 1000 and 375px; responsive reveal and re-hide refit; stale percentage widths dropped; drag follows the pointer and double-click resets; expanding a group leaves widths alone; Full Names untruncates, keeps paths on one line, persists and round-trips through the URL; the Observer column shrinks back when observer names arrive late; the pill popover lists every hop vertically, closes when the pointer leaves, pins on click without selecting the row, closes on Escape and on navigation, and opens on keyboard focus. Registered in `scripts/non-unit-tests.json` and `deploy.yml`. - On `master` the suite fails where it should, and restoring each fix's old behaviour (child-row indent, the late-observer refit) turns its case red. - `tests/unit/test-packets.js`: `distributeColumnWidths` (room to spare, rounding, widest-first capping, floors, dragged columns, the 60px minimum, overflow), the single chevron and no `::before` glyph, Full Names on flat and grouped observer cells, and the Full Names CSS. - `tests/unit/test-xss-escape-sinks.js` now builds the real `obsCellName` from source: escaping moved into it. Removing its `escapeHtml` turns both observer-cell tests red. - `test-issue-1189-composed-cell.js` and `test-issue-2012-clear-filters-selection.js` evaluate packets.js fragments with injected helpers and needed `obsCellName` / `showFullNames` added. - Existing packets E2E suites pass on this branch and on master against a CI-prepared fixture: scope column, #1128 (layout and multi-viewport), #1122, #1188, #1648, table sort, #1692, #1486. - Unit: 182 of 183 suites pass locally; `test-issue-1956-release-routing.js` could not create a temp directory in my sandbox and is unrelated. ## Browser validation Checked in Chromium against the fixture at 2000, 1920, 1280 (panel open and closed), 1100 and 375px, and with the seeded grouped row expanded and collapsed. Screenshots taken at each step; the table fills its container exactly, grouped rows show one arrow, and the pill popover lists full names. ## Not done, deliberately - **No width cap on Observer under Full Names.** A cap brings back the truncation the toggle removes, and Observer is already the first column to give way when space is short. - **Customizer.** The floors (60px flex minimum, 64px shrink floor, 32px expand, 58px Rpt) are hardcoded, per AGENTS rule 8; exposing them in the customizer can follow. - **Wrapping paths.** Rejected because of the fixed row height above; the pill popover is the way to read a long path. |
||
|
|
410c82c02c |
fix(ui): use consistent 48px navigation and channel buttons (#2078)
Fixes #2052. Navigation (`.nav-btn`) and channel icon (`.ch-icon-btn`) buttons now use the shared 48px house minimum. Remove competing 44px component/coarse-pointer rules and the legacy 32px mobile rule, and correct comments that confused the house preference with accessibility requirements. At 768px, user-added channel rows already clipped Remove; the larger Share target exposed the same constraint. Allow only those rows' controls to wrap so both actions remain within the sidebar. Navigation height and normal network-channel rows are unchanged. - Red `7787d9b`: rendered target assertions fail at 375/390/768/1280px. Green `e6ed9c8` changes CSS only. - Validation: all 183 standalone suites; 33 computed touch-target checks; 15 actual-page layout/click checks. Broader browser suite: 131 passed, 3 fixture-dependent skips. Three independent reviews found no required changes. - Browser verified: local Chromium and fixture-backed Go server; screenshots `coverage/2052-targets-375.png` and `coverage/2052-targets-768.png`. - E2E assertion added: `tests/e2e/test-channel-fluid-e2e.js:112`, covering dimensions, containment, overflow and normal Filter/Share clicks. Existing CI selection is retained; Chromium-required runs cannot silently skip the touch suite. - Test-only follow-up `5144503` waits for packet readiness before measuring navbar controls. Five fresh mobile contexts passed; size and click assertions remain intact. - No new settings, requests, dependencies or runtime JavaScript. ## Preflight overrides - External `run-all.sh` unavailable; repository checks run directly. - Local unit runner uses UTF-8 and `GITHUB_REF_NAME=local-validation` for the existing release harness. Browser navigation uses a 60s local budget for slow assets; repository assertions/timeouts are unchanged. |
||
|
|
2cfe9cbbc5 |
fix(a11y): raise the Scope Audit observed-chip contrast above 4.5:1 (#2070)
Closes #1996. ## Reproduced first The chips tint their own background — `color-mix(in srgb, var(--status-green) 16%, transparent)` — which darkens whatever surface sits behind them. With `--status-green-text` (green-700, `#15803d`) on top: | surface | composited chip | ratio | |---|---|---| | `--surface-0` `#f4f5f7` | `#d2eddf` | **4.04:1** | | `--card-bg` `#ffffff` | `#dcf6e5` | **4.38:1** | Both under the 4.5:1 the #1719 gate requires for normal text. The first row is the same ratio **and the same hex** @n30nex measured in the browser, which is how I know the model in the test agrees with what a visitor actually sees. ## Fixed on the text, not by thinning the tint Dropping the tint to 12% reaches 4.52:1. That is two hundredths above the line, and a margin that thin fails again the next time a surface value moves — the same trap #2039 hit with a threshold sitting inside the healthy band. One palette step darker gives **6.23:1** on white and **5.74:1** on `--surface-0`, and keeps the tint that makes a chip read as a chip rather than as plain text. - `--palette-green-800: #166534` added. The greens ran 300–700 while the blues already reach 900, so this fills the scale rather than inventing a colour. - `--sa-chip-observed-fg` defined per theme: green-800 in light, and the bright `#22c55e` **kept** in dark, where the chip already passed at 6.48:1 / 5.73:1. Dark is deliberately untouched. - The chip reads the variable, so the customizer still governs it and no literal enters a component. `.sa-chip-verified` needed no change: `scope-audit.js:106` only ever adds it alongside `sa-chip-observed` or `sa-chip-unobserved`, and it contributes an underline. So the fix covers both classes the issue names — worth stating, since the title mentions both. ## Tests `tests/unit/test-a11y-1996-scope-audit-chips.js`, 7 cases: both themes × both surfaces, that the chip still tints (so the suite cannot pass by testing nothing), that its colour comes from a variable rather than a literal, and a guard asserting green-700 **would** still fail — so a quiet revert to `--status-green-text` turns this red instead of passing. **Red-run confirmed:** with the old colour restored it fails at exactly 4.04:1 and 4.38:1, naming the composited `rgb(210,237,223)`. Two deliberate choices in the test: - **A separate suite, not a case in `test-a11y-1719`.** That suite's `parseColor` handles hex and `rgb()` only; teaching it `color-mix()` is a larger change than this fix. These chips are the only contrast-critical user of the function today. If a second appears, the two should merge, and the file says so. - **Derived from the stylesheets, not a rendered page.** The default Scope Audit fixture renders no chips at all, which is precisely why the existing browser coverage missed this. A stylesheet-derived check cannot be defeated by a fixture that shows nothing. `check-css-vars` passes (180 definitions, 0 undefined) and `test-test-inventory` passes with the new file classified. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7f4b357d89 |
fix(map): keep inspector controls clickable and ignore stale loads (#2048)
Fixes #1998 and #2030. Map Controls overlapped the Path Inspector toggle on desktop and intercepted its clicks; the controls are now offset by the collapsed and expanded inspector widths, with the mobile layout unchanged. Leaving the Map page while its node data was loading could log an invalidateSize error or let an old response overwrite a replacement map. Node-loading continuations and the deferred initialization callbacks are now tied to the Leaflet instance that started them, and obsolete results return before touching state or rendering. Reviewed against the code: the identity check is sound because destroy() clears the reference (map.remove() then map = null, public/map.js:2428-2433), so every guard is false after teardown and after replacement. Each await in loadNodes is followed by a check, as are the two deferred callbacks, the error branch and the finally. Moving 'nodes = data.nodes' after the observers await is deliberate: shared state is only mutated once the load is known to still own the map. The tests are the strong half. Overlap is asserted through elementFromPoint on the toggle's centre across five viewport widths and both pane states, rather than a forced click that would pass over an overlapping control, and 641/640 pin both sides of the breakpoint. Both extended suites are invoked by deploy.yml, one line each, which was checked rather than assumed. The commit chain is test-then-fix twice: e675100/7d736d1 and fd56ea2/823668b. One nit left to the author: the CSS repeats the pane widths from style.css:4532-4533 four times, so changing the pane width would silently misalign the controls, caught only as a waitForFunction timeout. Merged by the interim maintainer without a second human reviewer: CI and the review above are the independent checks. |
||
|
|
52b9474d7a |
feat(map): filter repeaters by region name (#1862) (#2022)
Fixes #1862 ## What Adds a **Region Scope** picker to the map controls: pick `#be` and the map keeps the nodes that declare `#be` or were seen carrying `#be` traffic. It combines with the #2006 scope-state filter and persists in localStorage the same way. While a region is picked, a small "Region: #be · reset" chip sits on the map itself, so the filter stays visible when the controls panel is collapsed (the default on phones) and can be cleared from there. ## API `/api/nodes` and `/api/nodes/{pubkey}` gain two fields on repeater/room rows: - `declared_regions`: named regions from the node's newest declared-regions answer, split by the same function the Scope Audit now uses for `declaredRegions` (`splitDeclaredRegions`, `cmd/server/scope_config_state.go`), so both pages list a repeater under the same names. `[]` means it answered and named no region. Absent means it never answered, other roles, no declared-regions source, or the declared-regions lookup failed. - `declared_regions_truncated`: present, and `true`, only when that answer was flagged as truncated, so the list is partial. Never `false`: the `nodes.configured_scope` source does not record truncation, so absence does not mean the list is complete. The observed side reuses `transported_scopes`. Documented in `docs/api-spec.md` and the served OpenAPI spec. ### Why no `?hashRegion=` query parameter The observed side lives in the in-memory store. Filtering it after the SQL `LIMIT`/`OFFSET` would corrupt `total` and paging, and the map pages through `/api/nodes`. Same reasoning as the Data path section of #2001. ## Map behaviour - Filtering is client-side over the nodes `fetchAllNodes` already loaded: no new request. One pass over the loaded nodes to build the picker counts, one Set lookup per node per render. The marker filter is `nodePassesMapFilters` (`public/map.js:219`) and the observer stand-down `observerLayerShown` (`:212`), both exported and tested. - The picker and hint count only nodes with a map position, the same test the marker filter applies first, so a count never promises markers the map cannot draw. - The observer layer stands down while a region is picked, for the same reason it does for the scope-state filter. - The popup lists declared and observed regions separately. A truncated declared answer carries the same `truncated` badge the Scope Audit shows. - Absence is not read as a finding: the hint under the picker says a node left off the map is not proof it lacks the region. ## Tests - Go: `node_declared_regions_api_test.go` covers `declared_regions` on list and detail endpoints, the no-source case, agreement with `/api/scope-audit`, `declared_regions_truncated` (truncated, truncated-empty, complete, configured_scope-only, newer untruncated answer, companion) and the OpenAPI schema. - JS: `test-issue-1862-map-region-filter.js` (30 tests) covers the pure pieces (evidence, counts, options, hint, popup rows, `nodePassesMapFilters`, `observerLayerShown`) and, at page level, runs the registered map page through `init()` and `loadNodes()` in a vm sandbox: picker built from loaded nodes, markers filtered by a stored region, observer pins standing down, popup rows, the change handler persisting, and the chip showing, resetting and rendering its text as text. - Mutation-checked: removing the region check, the observer stand-down, the picker build in `loadNodes`, the popup rows, the persist on change, the chip reset, or the truncated flag (Go or JS) each fails a test. `test-issue-2001-map-scope-state.js` still passes. - `go test ./...` in `cmd/server` passes; `check-css-vars` and `check-xss-sinks --diff` are clean. ## Staging validation Build `c646310f`, Chrome, no console errors: - Picking `#be`: chip "Region: #be · reset" at the top of the map, hint "221 nodes with a map position have evidence for #be: 129 declare it, 191 seen carrying its traffic. Absence here is not proof: ...". - Clicking reset: picker back to "All regions", stored choice cleared, chip hidden. - First version, same instance: `declared_regions` on 136 of 500 `/api/nodes` rows; returning to "All regions" blocked the main thread 5.4 s against 3.2 s for the existing Status filter returning to "All". The review measured the region filter's own added work at about 0.08 ms per render plus popup rows that are already built for every marker, so most of that time is the existing full re-render. ## Not verified - The chip placement at phone width, and on narrow desktop widths where it may sit under the expanded controls panel. - The truncated badge with real truncated answers (staging has none today). - `test-map-clustering.js` has one failing test on `upstream/master` too; untouched here. - Marker badges from the original issue body are not implemented; the popup rows are the per-node display. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2c6d7bb6ae |
feat(packets): add filter to All Observer dropdown (#1884)
Lets users type a prefix to filter the observer checkbox list, instead of scrolling a long list. --------- Co-authored-by: efiten <erwin.fiten@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6d7490e7da |
fix(tables): stop the trailing action column cropping its own button (#2010)
Reproduced on a live instance: 41px column for a 53px button at 1440px, 29px at 1024px, max-width:0 and overflow:hidden on the cell. Injecting the .col-action rule took it to 65px with nothing cropped. The new test fails 13 of 16 checks when only the CSS block is removed. |
||
|
|
296456f9c1 |
feat(map): colour and filter repeaters by scope-configuration state (#2006)
Closes #2001. Two review rounds plus a re-review; findings and evidence on the PR. Verified on a deployment against live data: the field over 1653 nodes, marker tints per filter state, the marker title and popup Scope row reaching the DOM, and the colorblind-preset cascade. The audit and the map are held to the same classification by an end-to-end test that fails when either side's wildcard handling drifts. |
||
|
|
a938176f83 |
fix(nodes): clearly mark the selected node in path chains (#1968)
Red commit: `0988bc0` (local browser: 3 passed, 8 behavior assertion failures before the fix). The selected node now has a compact outline in long “Paths Through This Node” chains, in the side panel and full detail page. Matching uses complete public keys without case sensitivity; same-prefix siblings and unresolved hops stay unmarked. Existing links, escaped names, warnings and ambiguity underlines are preserved. Fixes #1153. Its prerequisite #1144 is already merged. - E2E assertion added: `test-issue-1146-path-link-contrast-e2e.js:220`. The existing CI-selected harness passes 11 checks across 18-hop paths, both themes, desktop/mobile, and the renderer fallback. Review follow-up `bd8118f` verifies the marked ambiguous hop's dashed underline. - Browser verified: `http://127.0.0.1:55635`; desktop/mobile path screenshots were inspected. The broader smoke runner exited successfully with fixture-dependent skips. - Required frontend checks pass: 99 filter, 18 aging, 666 helpers. CSS variables, seven CSS self-tests, 31 XSS sink checks, 17 XSS gate self-tests and XSS diff preflight pass. - Three independent reviews found no blocking issues. Traversal remains linear with no new requests, settings, dependencies or cache invalidation; styling uses the existing customizer token. ## Preflight overrides - The external preflight runner is absent; corresponding scoped gates passed. Red browser evidence is local, with upstream CI approval tracked separately under the process in #1922. - Existing rapid-navigation map resize timer errors remain visible in browser logs and are outside this change. |
||
|
|
75dfa1f4fb |
fix(map): render an unobserved hash size as unknown, not as 1 byte (#1912)
## The bug
`map.js` turns a missing `hash_size` into `1`:
```js
var hs = node.hash_size || 1;
```
That field is **evidence**, not a default — `computeNodeHashSizeInfo`
populates it only from adverts it could read a size out of, so a node
with no countable advert in the retention window has no value at all.
Rendering that absence as `1` states a 1-byte configuration nobody
observed, and it does so in the one place where a reader is most likely
to act on it.
It is also inconsistent with the rest of the UI for the *same field on
the same node*:
| view | code | renders |
|---|---|---|
| node detail | `nodes.js:683` | `Hash Prefix: **Unknown**` |
| analytics prefix table | `analytics.js:1553` | `(**?**B)` |
| map popup + label + filter | `map.js:140`, `:1588`, `:1775` | `C8
**(1B)**` |
On analyzer.meshcore.cz right now, **701 of 1007 nodes** have
`hash_size: null`, so the map's 1-byte bucket is mostly nodes that were
never measured. The Byte Size filter has the same problem from the other
end: picking "1-byte" returns measured 1-byte nodes *and* every unheard
node, which makes it hard to use for the thing it exists for.
## The fix
- `roles.js`: shared `hashPrefixInfo(node)` → `{known, bytes, prefix}`,
so the map stops re-deriving the prefix in three places and the
"unknown" rule lives in one.
- `map.js`:
- **label** still draws a 1-byte prefix (it has to draw *something*) but
carries `.hash-unconfirmed` and its `aria-label` says `…, hash size
unknown`;
- **popup** says `Unknown`, matching `nodes.js` wording;
- **filter** gets its own `Unknown` bucket instead of folding unmeasured
nodes into 1-byte.
- `style.css`: dotted underline for the unconfirmed prefix — a shape cue
rather than a colour one, so it survives forced-colors and colour-vision
differences, consistent with the #1356 approach for these labels.
`nodes.js` and `analytics.js` are left alone: they already behave
correctly, and switching them to the helper would widen the diff without
changing behaviour. Happy to do it in a follow-up if you'd rather have
the call site count at zero.
## Tests
`node test-frontend-helpers.js` → **635 passed, 2 failed**; the two
failures are `favStar`, pre-existing on master (baseline run before this
change: 625 passed, 2 failed — same two).
10 new cases: `hashPrefixInfo` across missing / null / 0 / 1 / 2 /
3-byte inputs plus missing pubkey and a null node, and a guard asserting
`map.js` contains no bare `hash_size || 1` so this cannot quietly come
back.
## Browser validation
Headless Chromium against a live instance carrying real mesh data, same
viewport (`#/map?lat=50.038502&lon=14.570556&zoom=17`), unpatched vs
patched:
| | before | after |
|---|---|---|
| `aria-label` | `repeater hash C8` | `repeater hash C8, hash size
unknown` |
| label class | `mc-mb-label` | `mc-mb-label hash-unconfirmed` |
| filter buttons | `all,1,2,3` | `all,1,2,3,unknown` |
The four nodes in that viewport that *do* have evidence (`157E`, `381E`,
`FA74`, `C029`) render exactly as before.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
376c3e9f4a |
fix(packets): surface the transport region scope — detail pane row and a sortable Scope column (#1894)
## Summary `transmissions.scope_name` (#899) reached the database but never reached the UI. Two problems, one dead feature and one missing surface. ## 1. The detail pane's Scope row was dead `public/packets.js:3279` has rendered a **Scope** row since #899, gated on `pkt.scope_name != null`. It never fires in practice. `/api/packets` and `/api/packets/{id}` are served from the in-memory `PacketStore`. The store reads `scope_name` out of SQLite fine (`store.go:888`, `chunked_load.go:551` → `StoreTx.ScopeName`), but `txToMap()` did not put it in the JSON. Only packets old enough to have been evicted from the store — and thus served by the SQLite fallback in `db.go`, which does emit it — could ever show a scope. Verified against a live instance before the fix: ``` GET /api/packets/552e9687f1525537 → packet keys: ['_parsedPath','decoded_json','direction','first_seen','hash','id', 'observation_count','observations','observer_iata','observer_id', 'observer_name','path_json','payload_type','raw_hex','route_type','rssi','snr','timestamp'] ``` No `scope_name`. ### The NULL / "" distinction `StoreTx.ScopeName` was typed `string`, which collapses the two states the frontend distinguishes: | DB value | Meaning | UI | |---|---|---| | `NULL` | not transport-scoped | row hidden | | `""` | transport-scoped, region matched no configured key | muted "unknown scope" | | `"#be"` | matched region | the region name | `route_type` is **not** a usable proxy for that distinction: the ingestor writes NULL for a transport route whose `transport_code_1` is `0000` (`cmd/ingestor/db.go:1576` — `IsTransportScoped = route_type IN (0,3) AND Code1 ≠ "0000"`). So the field is now `*string`, with `nullStrPtr` preserving what `nullStrVal` collapsed. The two internal consumers (`TransportedScopes` #1751, `relayEntry.scope`) only care about non-empty named scopes and are unchanged in behaviour. ## 2. New: a Scope column on the packets table The scope was only reachable one packet at a time by opening the detail pane. It now has its own sortable column between Type and Observer, visible by default. The default view is **Group by Hash**, served by mappers that did not carry `scope_name` at all — so the column would have been empty in exactly the view most people look at. Both grouped paths now select and emit it: `groupedTxsToPage` in the store, and the dedicated grouped query in the DB fallback (v3 and legacy shapes). Rendering lives in `scopeCellHtml` (`public/app.js`, next to `transportBadge`) and is used on all three row-render sites — group header, expanded children, flat rows — so the column and the detail pane cannot drift apart. **Sorting** pins the empties last in both directions, as the nodes table already does for `default_scope`. Only ~8% of packets carry a scope, so an ascending sort would otherwise bury every scoped row under a wall of dashes. **Filtering**: `packet-filter.js` gains a `scope` field, so the cell is click-to-filter like Type and Observer, and `scope == "#be"` works in the filter bar. **Column prefs**: a `packets-known-cols` companion key. The `packets-visible-cols` array alone cannot distinguish "this column did not exist when you saved" from "you unchecked it", so any new column arrives silently hidden for every returning visitor. Keys absent from `known-cols` get the default treatment; keys the visitor actually hid stay hidden — there is a test for that second half specifically. ## Tests Each watched fail first. **Go** (`cmd/server/packet_scope_name_test.go`) - `txToMap` unit tests for all three states, including a JSON round-trip so a typed nil `*string` cannot pass as `null` - end-to-end through `/api/packets/{hash}` - `groupedTxsToPage` unit + end-to-end through `/api/packets?groupByHash=true`, across **both** the store-backed and DB-fallback paths - `transported_scopes_1751_test.go`: the "no scope" guard now covers both non-values (nil and a pointer to `""`) **Frontend** - `test-frontend-helpers.js`: `scopeCellHtml` three states + escaping - `test-packet-filter.js`: `scope` matching, case-insensitivity, and `FIELDS` registration - `test-packets-scope-column.js` (new Playwright e2e): header position, default visibility, one cell per row, em dash on non-transport rows, empties-last sorting, the Columns toggle, and the prefs backfill ## Verification Deployed and checked against a live instance: ``` /api/packets?groupByHash=true&limit=500 → scope_name present on 500/500, 59 with a matched region, 1 unknown-scope test-packets-scope-column.js → 7 passed, 0 failed cd cmd/server && go test ./... → ok ``` Two pre-existing failures, unrelated and equally red on an unmodified checkout: `test-e2e-playwright.js` "Customizer open does not overwrite server home config" and `test-observer-iata-1188-e2e.js` (timeout on `[data-loaded="true"]`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
aabbd50d27 |
feat(nodes): export the visible node list as MeshCore companion contacts JSON (#1889)
## What
Adds an **Export JSON** button to the Nodes topbar that downloads the
currently visible node list as a MeshCore companion-app config file:
```json
{
"contacts": [
{
"type": 2,
"name": "BE-MGU-RP03 | ON7YT",
"custom_name": null,
"public_key": "7a7a37d4819fb27440ed1439ca7d281fdecceb83b27e61a69745feb004d726a4",
"flags": 0,
"latitude": "51.07307",
"longitude": "5.5796",
"last_advert": 1786545431,
"last_modified": 1786545431,
"out_path_list": null
}
]
}
```
That is the exact shape the companion app itself writes, so the file
imports straight back into a companion app as contacts. The practical
use case is per-area: pick an area in the area filter, export, and hand
someone the repeaters for that region instead of having them wait for
adverts.
## Scope of the export
WYSIWYG — the button exports the rows the table is showing, in the
table's current order, so area, region, role tab, search, last-heard and
status filters all carry over. The button label shows how many contacts
the file will contain and is disabled when that count is zero.
## Field mapping
| JSON field | Source | Notes |
|---|---|---|
| `type` | `node.role` | repeater→2, companion→1, room→3, sensor→4;
unknown/empty→1 |
| `name` | `node.name` | unchanged, emoji included |
| `custom_name` | — | always `null` |
| `public_key` | `node.public_key` | full 64-hex |
| `flags` | — | always `0` |
| `latitude` / `longitude` | `node.lat` / `node.lon` | stringified, as
the format expects |
| `last_advert` | `node.last_seen` | RFC3339 → unix seconds |
| `last_modified` | mirrors `last_advert` | no separate source exists |
| `out_path_list` | — | always `null`; the companion app discovers
routes itself |
Nodes are skipped when they have no name, a pubkey shorter than 64 hex
chars, or no usable position (missing, non-numeric, or null island).
Filename: `corescope_nodes_<area|all>_YYYY-MM-DD-HHMMSS.json`.
## Shape of the change
The mapping lives in a self-contained `public/nodes-export.js`
(`window.NodesExport.buildContacts/filename/download`); `nodes.js` only
gains the button markup, a click handler and a small
`updateExportBtn()`. No backend change, no new API call — the export
reuses the already-fetched node list, so there is nothing per-node to
fetch.
## Tests
- `test-nodes-export.js` — field mapping and key order, role→type table,
skip rules, order preservation, filename format (added to
`test-all.sh`).
- `test-nodes-export-wiring.js` — `index.html` loads the module before
`nodes.js`; the button lives in the topbar and hands the filtered
`nodes` array plus `AreaFilter.getSelected()` to
`NodesExport.download()` (added to `test-all.sh`).
- `test-nodes-export-e2e.js` — Playwright: downloads the file, validates
the JSON shape per contact, asserts the button count matches the file,
and that narrowing the search shrinks the export set.
## Browser validation
Deployed to staging and checked in Chromium:
- Desktop 1400×900 — button renders at the right of the topbar next to
the count pills, `Export JSON (1962)`.
- Mobile 390×844 — topbar stacks, button visible, no horizontal overflow
(`scrollWidth == clientWidth`).
- E2E against staging: 1761 contacts exported,
`corescope_nodes_all_2026-08-12-164349.json`, shape validated, search
narrowing confirmed.
- With the `BE-LIM` area filter active: 44 contacts,
`corescope_nodes_BE-LIM_2026-08-12-164445.json`, type histogram `{1: 1,
2: 42, 3: 1}`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8c48f2b49a |
feat(#1784): wire display consumers to path trust threshold (#1841)
Fixes #1784 ## Summary Step 4 of the #1784 multi-PR project — wires all frontend display consumers to respect the configured `pathTrust.minHashBytesForMapping` value. **Depends on:** #1840 (must be merged first — provides `MC_meetsPathTrust` / `MC_pathBelowTrust` / `MC_getPathTrustThreshold` helpers and `PATH_TRUST` global). ## Changes ### `map.js` — trust-gated route display - `drawPacketRoute` checks `MC_pathBelowTrust` before drawing polylines - When all path hops are below the configured threshold, shows a "Route not displayed" message with config guidance instead of speculative polylines - Cleans up the trust message control when a new route is drawn ### `analytics.js` — subpath trust filtering - `renderTable` in `renderSubpaths` filters route patterns whose hops are below trust threshold - Combined filter logic: both the 1-byte hide toggle AND trust threshold are applied together - Info line shows which filters are active ("1-byte hide + 2-byte trust", etc.) - No-data message explains which filters caused exclusion ### `route-view.js` — speculative path annotation - Path picker groups tagged with `belowTrust` flag when any hop doesn't meet the configured threshold - Speculative paths show `(speculative, <N-byte hops)` annotation with tooltip explaining the trust threshold - Tooltip includes guidance on changing `pathTrust.minHashBytesForMapping` in config.json ### `live.js` — Paths Through widget - `_pathHopsBelowTrust()` helper checks whether all hops in a path are below trust threshold - Widget fallback message distinguishes between "1-byte filtered" (display toggle) and "N-byte trust threshold" (server config) - Message includes the config key for operators to adjust ### `nodes.js` — confidence weight adjustment - `modeWeight` initialization considers the trust threshold - Hash modes below threshold get zero confidence weight - Bucket-0 (legacy/unknown) excluded at threshold >= 2 per #1784 bucket-0 policy ## Testing `test-issue-1633-hide-1byte-hops.js`: - 5 new source-grep guards verifying each consumer references the trust threshold helpers - All 26 tests passing (21 existing + 5 new) ## Files changed (6 files, +136/-6) ``` public/analytics.js | 28 ++++++++++++++++++--- public/live.js | 19 ++++++++++++++- public/map.js | 21 ++++++++++++++++ public/nodes.js | 8 ++++++ public/route-view.js | 16 +++++++++++- test-issue-1633-hide-1byte-hops.js | 50 ++++++++++++++++++++++++++++++ ``` --- **Depends on:** #1840 **Written by:** DeepSeek V4 Pro in Max Mode |
||
|
|
8c3e397d39 |
fix(1846): drop 1200px cap on .observers-page (#1847)
Red commit:
|
||
|
|
707d70c738 |
fix(packets): clamp .col-details to one line on mobile (#1770 S path) (#1805)
## Summary Partial fix for #1770 (S quick-fix path only; L refactor remains as follow-up). The packets-view virtual-scroller assumes a constant `VSCROLL_ROW_HEIGHT`, but the base rule at `public/style.css` L1097 lets `td.col-details` wrap on narrow viewports (`white-space: normal; word-break: break-word`). Wrapped rows produce variable row heights → visible jitter when scrolling past ~900px on iOS. **Quick-fix (S path):** under the existing `@media (max-width: 640px)` block in `public/style.css`, clamp `.col-details` to a single line: ```css .data-table td.col-details { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ``` Trade-off accepted in triage: Details column truncates on mobile in exchange for smooth scrolling. The base rule keeps wrapping on desktop (≥641px) so nothing changes there. **Out of scope:** the full L-path fix (per-row measurement, `_rowHeightsPx[]`, cumulative offsets, re-measure on hop-resolver finalize) — tracked separately on #1770. ## TDD - **Red commit** `7f58bedc` — adds `test-issue-1770-mobile-row-clamp.js`, a CSS-grep test (same pattern as `test-issue-1364-pill-no-clamp.js`) that walks every `@media (max-width: 640px)` block in `public/style.css` and asserts a `.col-details` rule declares `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`. Verified to FAIL on master (assertion failure, not a parse error) and PASS after the CSS change. - **Green commit** `d46271b8` — applies the 5-line CSS clamp inside the existing mobile breakpoint at L2362. ## Files touched - `public/style.css` (+13) - `test-issue-1770-mobile-row-clamp.js` (+101, new) ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all gates pass (PII, branch scope, red commit, css-vars, css self-fallback, LIKE-on-JSON, sync migration, async-migration, XSS). No warnings. --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
735d9eb516 |
fix(#1715): dark-theme role swatches via per-theme CSS tokens (#1757)
## Summary Dark-theme variants of the neighbor-graph role swatches (`#ngRoleChecks` labels on `/analytics?tab=neighbor-graph`) still failed WCAG AA after #1720's light-theme fix because the swatches used inline `style="color:#..."` from `customize.js` `DEFAULTS.nodeColors` (palette-700) — bypassing the theme tokens entirely. Measured before: | Role | Color | vs `#1a1a2e` (dark) | |---|---|---| | repeater | `#dc2626` | 3.53:1 ❌ | | companion | `#2563eb` | 3.30:1 ❌ | | observer | `#8b5cf6` | 4.02:1 ❌ | ## Fix - Defines `--role-{repeater,companion,room,sensor,observer}` in `:root` (palette-700, ≥4.5:1 on white) and overrides them in both dark blocks (`[data-theme="dark"]` + the `@media (prefers-color-scheme: dark)` mirror) with palette-400/500 shades that clear AA on `#1a1a2e`. - Refactors the neighbor-graph swatch DOM in `public/analytics.js` from inline `style="color:${hex}"` to class-based `<span class="role-swatch role-swatch--{role}">`, with matching CSS rules that read the tokens. - Removes all 5 `#1715` entries from `tests/a11y-allowlist.yaml` per the issue's acceptance criteria. After: | Role | Light (vs `#fff`) | Dark (vs `#1a1a2e`) | |---|---|---| | repeater | `#dc2626` 4.83:1 | `#ef4444` 4.53:1 | | companion | `#2563eb` 5.17:1 | `#3b82f6` 4.64:1 | | room | `#15803d` 5.02:1 | `#16a34a` 5.18:1 | | sensor | `#b45309` 5.02:1 | `#d97706` 5.35:1 | | observer | `#7c3aed` 5.70:1 | `#a78bfa` 6.27:1 | ## Tests - `test-a11y-1715-dark-role-swatches.js` — CSS-driven WCAG AA probes for the 5 per-theme `--role-*` tokens plus markup invariants (no inline color span; class names present in `#ngRoleChecks` block). - Red commit: `a09ec21c` — fails on assertion with 12 below-threshold/markup probes. - Green commit: `f87dcd64` — all probes PASS. - `tests/a11y-allowlist.yaml` shed all 5 entries; the umbrella `test-a11y-axe-1668.js` (CI) is now the live-browser net for those cells. ## Preflight overrides - `check-xss-sinks.sh` flags `public/analytics.js:2502` (label "observer" appears in an `innerHTML=\`tpl\`` line). The flagged token is a hardcoded literal string — no user-controlled data flows into that template. No template content changed in this PR; the flag is preexisting noise from the heuristic scan and the gate ultimately marks ✅ pass. Fixes #1715 --------- Co-authored-by: clawbot <clawbot@kpa.local> Co-authored-by: clawbot <bot@example.com> |
||
|
|
a344ae0a12 |
fix(#1719): contrast root causes — active-btn / skew-badge / role-swatch / status-green (#1720)
## Summary Fixes the four recurring color-contrast root causes #1719 identifies behind ~320 axe violations on PR #1707's expanded gate. All fixes are token-based; no hardcoded hex introduced. ## TDD - **Red:** `151db732` — `test-a11y-1719-contrast-root-causes-e2e.js` asserts WCAG AA on all 4 patterns; failed with 12 sub-threshold probes. - **Green:** `dd26554e` — fixes below; test now reports 11/11 PASS. ## Patterns + measured contrast (before → after) | # | Surface | Before | After | Note | |---|---|---|---|---| | P1 | `.rf-range-btn.active` / `.clock-filter-btn.active` / `.subpath-jump-nav a` / `#ptCheckBtn` / `#ptGenBtn` | `#fff` on `--accent` (#4a9eff) = **2.75:1** | `--text-on-accent` on `--accent-strong` = **4.95:1** | Consolidated into ONE grouped `.btn-active-accent, ...` rule; inline buttons now use the shared class | | P2 | `.skew-badge--no_clock` (dark theme) | `#fff` on `--text-muted` (#d1d5db) = **1.47:1** | `#fff` on `--skew-badge-no-clock-bg` (#4b5563) = **7.56:1** | New dedicated token, both themes | | P3 | Neighbor-graph role swatches, light theme on white | room 3.30:1 / sensor 3.19:1 / observer 4.23:1 | room **5.02** / sensor **5.02** / observer **5.70** | `customize.js` defaults bumped to palette-{green/amber/purple}-700 | | P4 | `.analytics-stat-card` text in `--status-green` on white | **2.28:1** | new `--status-green-text` = #15803d → **5.02:1** | `--status-green` background token unchanged (still #22c55e); inline text usages routed to the new token | ## Why this unblocks #1707 #1707's 320 axe color-contrast hits decompose into: - 137× single-rule `.skew-badge--no_clock` → P2. - ~N×4 active-button surfaces (rf-health / clock-health / subpaths / prefix-tool) → P1. - Role-swatch text on `/#/analytics?tab=neighbor-graph` (light) → P3. - `.analytics-stat-card` text on `/#/analytics?tab=nodes` (light) → P4. After this merges, the next CI run on #1707 should see the expanded gate go green (or down to a small ≤5 residual the operator can triage separately per the issue's acceptance criteria). ## Local axe gate `BASE_URL=… node test-a11y-axe-1668.js` was **NOT** run locally — the sandbox's bundled chromium fails to boot Playwright (known issue). CI on this PR runs the same gate against the staging fixture; relying on that. The dedicated test `test-a11y-1719-contrast-root-causes-e2e.js` is CSS+JS-parse-driven (no browser) and runs in <100ms — it's the regression net for these 4 patterns specifically. ``` $ node test-a11y-1719-contrast-root-causes-e2e.js PASS [P1] theme=light .rf-range-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=light .clock-filter-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=light .subpath-jump-nav a fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .rf-range-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .clock-filter-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .subpath-jump-nav a fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P2] theme=light .skew-badge--no_clock fg=#fff bg=#4b5563 ratio=7.56:1 PASS [P2] theme=dark .skew-badge--no_clock fg=#fff bg=#4b5563 ratio=7.56:1 PASS [P3] theme=light nodeColors.room on white fg=#15803d bg=#ffffff ratio=5.02:1 PASS [P3] theme=light nodeColors.sensor on white fg=#b45309 bg=#ffffff ratio=5.02:1 PASS [P3] theme=light nodeColors.observer on white fg=#7c3aed bg=#ffffff ratio=5.70:1 PASS [P4] theme=light .analytics-stat-card text color (--status-green-text) fg=#15803d bg=#ffffff ratio=5.02:1 PASS [P4] theme=dark .analytics-stat-card text color (--status-green-text) fg=#22c55e bg=#232340 ratio=6.65:1 PASS: all 4 root-cause patterns ≥ 4.5:1 in both themes (issue #1719) ``` ## Out-of-scope (intentional) - Other text-on-light `var(--status-green)` usages in `nodes.js` (Critical/Valuable labels): different surface, not the analytics-stat-card pattern #1719 calls out. Tracked under analytics audit umbrella. - Hardcoded `var(--status-green, #2ecc71)` fallback in `nodes.js` lines 648/666: same scope deferral. - Allowlist entries: none added per the issue's acceptance criteria. Fixes #1719. --------- Co-authored-by: clawbot <clawbot@kpa.local> Co-authored-by: Kpa-clawbot <bot@openclaw.local> |
||
|
|
293efdb647 |
fix(#1705): subpath-selected hop-prefix contrast BLOCKER (dark, 1.87:1 → ≥4.5:1) (#1708)
## Summary Fixes the BLOCKER half of #1705: `.subpath-selected .hop-prefix` contrast in `public/style.css`. | | Before | After | |---|---|---| | background | `var(--accent)` = `#4a9eff` | `var(--accent-strong)` = `#2563eb` | | color (primary) | `#fff` | `var(--text-on-accent)` = `#f9fafb` | | color (hop-prefix) | `rgba(255,255,255,0.6)` | `var(--text-on-accent)` | | measured contrast (hop-prefix) | **1.87:1** (composite over `--accent`, dark) | **4.95:1** (light + dark) | Pure token swap onto the existing `--accent-strong` / `--text-on-accent` pair already used by `.badge-selected`, `.filter-bar .btn.active`, `.dropdown-item:hover` etc. No new hex literals. Light and dark themes both pass WCAG AA body text (≥4.5:1). ## TDD trail - Red: `033f8e4c` — `test-a11y-1705-subpath-hop-prefix-e2e.js`. Parses `public/style.css`, resolves the relevant tokens per theme, composites the alpha-bearing text over the rendered background, asserts WCAG contrast ≥ 4.5:1. Failed with `ratio=1.87:1` on both themes — the exact value cited in #1705. - Green: `db6b9dd0` — CSS fix. Test now reports `composite=#f9fafb, ratio=4.95:1` on both themes. Why a dedicated test (not just `test-a11y-axe-1668.js`): `.subpath-selected` is a click-state class, so the umbrella axe gate never sees it during initial-paint scans. This is the canonical "state-only" a11y regression class — the umbrella gate is structurally blind to it. ## Out of scope (documented in #1705 for separate follow-up) - The **a11y audit probe correctness fix** (alpha-composite + parent-bg walk) lives in workspace tooling (`workspace-meshcore/a11y-audit/audit.py`), not in this repo. The probe-correctness write-up is captured in #1705 itself; this PR is exclusively the CSS BLOCKER + regression test. - "Other rgba-based dark-mode contrast surfaces" — per the issue's Out-of-scope section, those get filed separately if discovered. ## Local verification ``` $ node test-a11y-1705-subpath-hop-prefix-e2e.js PASS theme=dark bg=#2563eb text=#f9fafb ratio=4.95:1 PASS theme=light bg=#2563eb text=#f9fafb ratio=4.95:1 ``` The full `test-a11y-axe-1668.js` gate could not be exercised on this sandbox (Chromium SIGTRAPs against the host kernel — unrelated to this change). CI runs it on Ubuntu where the umbrella ruleset already enforces the 0-violation policy. ## Browser verified CSS-only change in a CSS-variable swap. Computed values are deterministic from the stylesheet and asserted by the new test; no JS / DOM / render-path is touched. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all gates clean. Fixes #1705 --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: Kpa-clawbot <bot@clawbot> |
||
|
|
d954ea7444 |
feat(#1668): axe-core CI gate for WCAG AA color-contrast (M5) (#1696)
Partial fix for #1668 (M5 of 6). After M1 (audit), M2 (color tokens, #1676), M3 (typography floor, #1679), and M4 (per-route polish, #1681) cleared ~95% of contrast/typography violations, M5 **locks in the wins** by adding an axe-core CI gate that fails the build on any new WCAG AA color-contrast regression. ## What's in the box - `test-a11y-axe-1668.js` — Playwright + `@axe-core/playwright`. Runs every major CoreScope route × `{dark, light}` at 1200×900 desktop, injects axe, runs only the `color-contrast` rule, asserts net violations === 0. - `test-a11y-axe-1668-selftest.js` — fast, deterministic, browser-free unit test that exercises the YAML allowlist parser, the `violationAllowed` matcher, and the route/theme metadata. Runs in the JS unit block (no browser needed). - `tests/a11y-allowlist.yaml` — operator-flagged false-positive allowlist. **0 entries at M5 baseline.** ## Allowlist format Each entry MUST cite a GH issue # and an `expires_at` date. Missing fields = refused. Expired `expires_at` = refused (warning logged). This **forces a periodic revisit** — no permanent suppressions. ```yaml - route: /analytics?tab=channels selector: ".some-known-stale-element" rule: color-contrast issue: 1234 expires_at: 2026-09-01 ``` ## Routes covered (19 × 2 themes = 38 cells) `/`, `/packets`, `/nodes`, `/channels`, `/live`, `/map`, `/observers`, `/compare`, `/analytics?tab={overview,rf,topology,channels,hashsizes,collisions,roles,airtime}`, `/audio-lab`, `/customize`, `/replay`. ## TDD red→green - **RED** (`08adafdb`) — adds the gate + deliberately regresses `--text-muted` from `palette-gray-700` (~10:1) to `#9ca3af` (~2.4:1). axe-core fails on every light-theme cell. - **GREEN** (`f62fb1e0`) — restores the M2 token. Net violations = 0 across all 38 cells. ## Scope discipline - Only `color-contrast` (matches M2/M3/M4 scope). M6 owns `image-alt`, `aria-required-attr`, `label`, mobile viewports, and letsmesh A/B. - No new design tokens. - M2-M4 tokens untouched. ## CI wiring - `.github/workflows/deploy.yml:155` — selftest in JS unit block. - `.github/workflows/deploy.yml:367` — real axe browser run in the Playwright E2E block after the fixture server is up. ## Deps `@axe-core/playwright@4.11.3` + `axe-core@4.12.1` added to `devDependencies`. Pinned versions. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
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> |
||
|
|
6aa5146b93 |
fix(#1660): FE warm-up banner reads X-Corescope-Load-Status + polls /api/healthz (#1683)
## Summary Partial fix for #1660 — adds an FE-only global warm-up banner that surfaces server-side load state to users instead of letting "data may be incomplete" look like silent breakage. Implements sub-deliverables **(1)** and **(3)** from the triage. Sub-deliverable (2) (per-card "recomputing" pill) is deferred — it depends on a new server-side `recomputer.first_pass_done` flag that pairs with #1659. ## What it does - New `public/warmup-banner.js` mounts a sticky `role="status"` live region at the top of `<body>`. Pure helper `getWarmupMessages()` is fully unit-tested in isolation. - Consumes both signals the server already exposes: - `X-Corescope-Load-Status` response header (set by `cmd/server/chunked_load.go:446` on every API response) — captured via a thin `window.fetch` wrapper. - `GET /api/healthz` — polled every 30s while not in steady-state, torn down once `ready=true` AND `from_pubkey_backfill.done=true`. - Messages per acceptance criteria: - `loading` → "⏳ Loading historical data — counts may be incomplete." - `from_pubkey_backfill.done=false` → "Backfilling pubkey index: 12,400 / 87,500 (14%)" - `ingest_liveness.<src>.lastReceiptUnix` older than 5 min → "No packets from `<src>` in N min." - Banner fades out (opacity + max-height transition) once steady-state is reached. ## Files - `public/warmup-banner.js` — new module (pure helpers + DOM mount + poll + fetch interceptor). - `public/style.css` — `.warmup-banner` rules; all colors via existing `--warn-bg` / `--warn-text` / `--warning` CSS variables (customizer-safe, no inline hexes). - `public/index.html` — loads `warmup-banner.js` immediately before `app.js` so the fetch wrapper is installed before other modules issue requests. - `test-warmup-banner.js` — 8 tests: 6 pure-helper + 2 vm-DOM E2E that stub `/api/healthz` returning `ready:false` → asserts banner visible, then flips to `ready:true` → asserts the `warmup-banner--hidden` class is applied (sub-deliverable 3). ## TDD red → green - **Red:** `ca5f9837` — `test(#1660): RED — failing tests for warmup banner message derivation` — stub `getWarmupMessages` returns `[]`; CI fails on 3 assertion failures (compiles cleanly, fails on `assert.ok(msgs.length >= 1)` etc — not on import/build). - **Green:** `0d07efdf` — `feat(#1660): GREEN — warmup banner reads X-Corescope-Load-Status + polls /api/healthz` — implementation lands; all 8 tests pass. ## Test output ``` warmup-banner.js (#1660): ✅ exports getWarmupMessages and shouldShowBanner ✅ loading header alone produces a "historical data" message ✅ from_pubkey_backfill.done=false produces a progress message with pct ✅ stale ingest source >5min produces a "No packets from" message ✅ steady-state ready=true + backfill done + fresh ingest → no banner ✅ isSteadyState reflects ready+backfill predicate ✅ E2E: stub /api/healthz ready=false → banner visible ✅ E2E: flip /api/healthz to ready=true → banner fades (hidden class) passed=8 failed=0 ``` ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — **clean** (PII / branch scope / red commit / CSS-var defined / CSS self-fallback / LIKE-on-JSON / sync migration / async-migration gate / XSS sinks all PASS, no warnings). ## Performance - Poll runs every 30s and only while `ready=false || from_pubkey_backfill.done=false`. Stops immediately on steady state. No hot-path impact. - Fetch wrapper adds one `.then()` per response to read a single header — O(1). - Banner DOM is one `<div>` with a `<ul>` of ≤3 `<li>`s. Re-render is a single innerHTML set. ## Out of scope (explicit) - Sub-deliverable (2) — per-card "↻ Recomputing…" pill. Requires a new `recomputer.first_pass_done` field on `/api/healthz` (small `cmd/server/analytics_recomputer.go` addition) and is grouped with the #1659 recomputer redesign. Not in this PR. - No backend code changed. Partial fix for #1660. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
626900a22a |
fix(#1668): typography pass — 14px body / 12px+500 chip floor (M3) (#1679)
Red commit:
|
||
|
|
f0addfdabf |
fix(#1668): palette indirection + WCAG AA token bumps (M2 + #1671) (#1676)
Red commit:
|
||
|
|
89eade6e7b |
M6: emoji → Phosphor — final sweep, lint gate, carry-forwards (#1648) (#1654)
Red commit:
|
||
|
|
1116801b2f |
M5: emoji → Phosphor Icons — settings & customize (#1648) (#1653)
**Red commit:** `851cc8c3a024b1675558092d772444bf4f1ec625` — failing test on a stub branch (will link CI run after PR opens). Partial fix for #1648 (M5 of 6). **Do NOT close the tracking issue** — M6 (server-side residual emoji sweep + lint gate) still pending. ## Per-file swap counts | File | Phosphor `<use>` refs | Notes | |---|---|---| | `public/customize.js` | 20 | DEFAULTS → `ph:<name>` tokens; render path keeps legacy emoji branch (back-compat) | | `public/customize-v2.js` | 26 | same as v1; cv2 overrides path unchanged | | `public/home.js` | (helpers added) | `_renderHomeGlyph` / `_renderHomeLabel` accept both `ph:<name>` and legacy emoji | | `public/geofilter-builder.html` | 5 | clear / undo / save / load buttons (+inline `.ph-icon` CSS) | | `public/audio.js` | 1 | audio unlock prompt | | `public/filter-ux.js` | 5 (3 new) | help popover star + close, saved-filter delete | | `public/style.css` | 0 | `#chList .ch-share-btn::before { content: '📤' }` removed; JS now renders an inline sprite | | `cmd/server/routes.go` | (6 `ph:` tokens) | onboarding home defaults updated in lockstep with customize-v2.js | ## Operator config back-compat — PROMINENT Per design call #1 (user-locked): existing operator-stored emoji values in `config.json` / `localStorage` are **NOT** touched. The render path supports both: ```js function renderConfigGlyph(value) { var m = String(value || '').match(/^ph:([a-z][a-z0-9-]+)$/); if (m) return '<svg class="ph-icon"><use href="/icons/phosphor-sprite.svg#ph-' + m[1] + '"/></svg>'; return esc(value); // EMOJI-OK-LEGACY-RENDER — operator-stored emoji/text path } ``` Defaults flipped to `ph:<name>` tokens, so new operators (and operators who hit "Reset to Defaults") see Phosphor sprites. Operators with stored emoji values continue to see their emoji exactly as before. Verified end-to-end (see E2E (b) below). ## cmd/server/routes.go — changed in lockstep Per design call #2: the home-defaults `steps` / `footerLinks` mirror the JS DEFAULTS, so they MUST update together. routes.go now emits `ph:<name>` tokens; the frontend home-render path resolves them. Existing tests (`TestConfigThemeHomeDefaults`) still pass — they assert structure, not glyph values. ## E2E assertions added - `test-issue-1648-m5-emoji-scan.js` — per-file zero-emoji + ph-token DEFAULTS + sprite presence - `test-issue-1648-m5-icons-e2e.js`: - (a) customize chrome — tabs/header rendered as sprites; chrome text icon-free - **(b) back-compat — injects fake `🐙` operator step into localStorage, reloads, opens customize, asserts the emoji renders verbatim in both the input value AND the live preview span; asserts the ph-token step renders as a sprite** (design call #1 in action) - (c) `/channels` modal sprite count - (d) `/audio-lab` sprite presence - (e) `geofilter-builder.html` control buttons sprite-driven - (f) every `<use>` resolves to a defined symbol id ## Out of scope (M6 cleanup) - cmd/server/routes.go residual server-rendered emoji **not** tied to customize defaults (none found by my grep — file already audited) - `make lint-no-emoji` CI grep gate (M6 owns it) - `public/icons/README.md` workflow doc cross-stack: justified — design call #2 requires Go + JS update together. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
b812a98a71 |
M3: emoji → Phosphor Icons — detail panes & badges (#1648) (#1651)
Red commit:
|
||
|
|
3062745437 |
M2: emoji → Phosphor Icons — page headers & table chrome (#1648) (#1650)
Red commit:
|
||
|
|
55e4d957b1 |
M1: emoji → Phosphor Icons — top-nav, mobile nav, Compare (#1648) (#1649)
Red commit:
|
||
|
|
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> |
||
|
|
e2212f5015 |
feat(nodes): per-node Reach page + GET /api/nodes/{pubkey}/reach (v2, review-complete) (#1627)
Re-submission of #1625 (which was merged early, then reverted in #1626) — now with **all three round-1 reviews addressed** so it lands in one hardened state instead of as post-merge follow-ups. ## What Per-node **Reach** view: a standalone page (`#/nodes/{pubkey}/reach`) + a node-detail section + `GET /api/nodes/{pubkey}/reach`. It shows which nodes a node has a **stable two-way RF link** with, derived from raw `path_json` adjacency (a path travels origin→observer, so `[A,B]` ⇒ B heard A). A link is bidirectional when both directions have observations; the **bottleneck** (weaker direction) rates two-way reliability. Nodes are identified only by **unique 2–3 byte** path prefixes (1-byte collides → excluded). ## Review fixes folded in vs #1625 **Performance (Carmack):** hard scan LIMIT (200k) + modest prealloc; `json.Unmarshal` replaced by a single-pass `parsePathTokens` (100k-row scan 2.2M→1.3M allocs, 344→203ms); memoized resolver; size-hinted maps (attribution over 100k rows: 102 allocs); `context.Context` plumbed; cache `RWMutex` + evict-oldest (no full wipe); singleflight dedup; degree/rank from a 60s shared snapshot; bench rewritten (ReportAllocs, 1k/10k/100k, mixed-payload, isolated attribution). **Correctness/safety + tests (Independent + Kent Beck):** pubkey validation → 400; error logging instead of silent swallow (first_seen / degree / marshal→500 / discarded rows); `public_key=?` index use; canonical `PayloadADVERT`; `min()` builtin; documented cache-slice immutability; mux ordering comment. New tests: scanReachRows decode, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation across rows, HTTP-level attribution (asserts non-zero we_hear/they_hear), 400/404/blacklist/cache-hit. **UI / a11y / Tufte:** in-map legend (tiers + thresholds); dropped the colour+width double-encoding (constant width, colour-only); colour-blind glyphs (●●●/●●/●) + tier title beside the bottleneck number; dark-theme `--link-*`; lighter table (horizontal rules, sentence-case headers); map built once + link layer updated in place on toggle (no flicker); time-range no longer flashes a loader; `destroy()` generation guard; statCard escaping; scoped `@media print` to `#nq-report`; `fieldset/legend` + `for/id` toggles; `aria-pressed` / `aria-live` / back-link `aria-label`; "distance (km)" + bottleneck tooltip + no-GPS note; inline styles → CSS; decorative emoji removed. **Docs:** api-spec documents the 5-min cache, 200k scan cap, and 400. ## Testing - `cmd/server` full suite green; reach unit + endpoint + bench all pass. - `eslint public/*.js` (no-undef) and the XSS-sink gate clean. - E2E updated: request status checks + exact (non-tautological) toggle assertions + hard map-render assert. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## TDD-history note (Kent Beck gate) This branch carries production + tests together, not a fabricated red→green sequence. That's deliberate: the branch was rebased onto upstream and the intermediate SHAs were squashed, so reconstructing a "failing-test-first" commit after the fact would be theatre, not evidence — and rewriting history to stage it would be dishonest. The behaviour is instead covered by a comprehensive, anti-tautological suite (directional attribution edges, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation, HTTP-level attribution asserting non-zero counts, scan-cap truncation, zero-reach 200-not-404, companion mis-attribution, cache eviction). Requesting maintainer acceptance of the work on test *substance* rather than commit *choreography*; the net-new-UI exemption is not claimed for the server endpoint. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: meshcore-bot <bot@meshcore> |
||
|
|
9c5faab1e4 |
Revert "feat(nodes): per-node Reach page (#1625)" (#1626)
Reverts #1625. #1625 was merged before the round-1 reviews (Independent / Kent Beck / Tufte) were addressed. Reverting to land it cleanly: a fresh PR will re-add the feature with the perf pass, the backend correctness/safety + test-coverage fixes, and the UI/a11y (Tufte) batch folded in, so it goes through review in a single hardened state rather than as a string of post-merge follow-ups. No functional loss — the feature returns in the replacement PR. |
||
|
|
47f85f6c4c |
feat(nodes): per-node Reach page + GET /api/nodes/{pubkey}/reach (directional link quality) (#1625)
## What
Adds a per-node **Reach** view that answers "how well does this specific
node hear, and get heard by, its neighbours?" — both as a standalone
page (`#/nodes/{pubkey}/reach`) and as a section on the node detail
page.
New endpoint: **`GET /api/nodes/{pubkey}/reach`**.
## What it measures
For the target node it derives, from raw `path_json` adjacency (a path
travels origin→observer, so in `[A,B]` B received A directly):
- **Directional link counts** per neighbour: `we_hear` (how often we
received them) vs `they_hear` (how often they received us).
- **Bidirectional / bottleneck**: a link is two-way stable when both
directions > 0; the weaker direction is the bottleneck and rates real
two-way reliability.
- **Importance**: neighbour degree + rank, relay-observation volume,
bidirectional-link count, direct-observer count.
- **Direct observers**: who received the node at 0 hops, with SNR.
Reliability rule: a neighbour is only attributed when its pubkey
**prefix is unique** at the path's byte length (collisions are skipped,
never misattributed).
## UI
- Standalone Reach page + node-detail section.
- Reusable bidirectional link map (OSM) with links coloured by
bottleneck.
- Incoming/outgoing toggles to isolate each direction.
## Naming note (deliberate, no collision)
This is distinct from the existing **per-observer reachability** in
topology analytics (`ReachNode` / `ObserverReach` / `perObserverReach`).
This PR adds its own `NodeReach*` response structs in a new
`node_reach.go` and a new `/api/nodes/{pubkey}/reach` route — there are
no symbol or route collisions (verified: `go build ./...` clean). Happy
to rename to disambiguate further (e.g. "Link Quality") if you'd prefer
to reserve "Reach" for the per-observer feature.
## Testing
- `cmd/server`: endpoint shape/404/limit-clamp + unit tests for token
derivation and directional attribution, plus a scan benchmark — all
pass.
- Frontend: helper tests + Reach-page E2E (`test-node-reach-e2e.js`),
standalone route + incoming/outgoing toggles.
- `go build ./...` and `eslint public/*.js` (no-undef) clean.
## Docs
Design spec, implementation plan, and the `GET
/api/nodes/{pubkey}/reach` API contract are included under `docs/`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a4776557ae |
feat(#1290): use firmware repeat:on|off hint to exclude listener-only observers from disambiguator (#1624)
Closes #1290. cross-stack: justified — backend persists firmware-side `repeat` hint to a new observers column, frontend surfaces the listener/repeater status as a badge on the observers list and node-detail Heard By table per the issue's UI acceptance criterion. ## What Firmware 1.16 publishes a `repeat: on|off` flag in the MQTT `/status` JSON (confirmed by @cwichura on the issue thread — see [`MQTTMessageBuilder.cpp:58`](https://github.com/agessaman/MeshCore/blob/b45373a31f111fb0de98bb3b168226d09ceadc47/src/helpers/MQTTMessageBuilder.cpp#L58) in `agessaman/MeshCore mqtt-bridge-implementation-flex`). Listener-only observers (`repeat:off`) by firmware contract never relay packets, so they cannot legitimately be a hop in someone else's resolved path. This PR plumbs the hint end-to-end so the disambiguator stops considering them. ## How * **`internal/dbschema`**: idempotent `can_relay INTEGER DEFAULT 1` migration on `observers`, plus `AssertReady` probe (server fatal-logs if absent). Mirrored in `cmd/ingestor/db.go` `CREATE TABLE` for fresh DBs. Annotated `PREFLIGHT: async=true` — `DEFAULT 1` is constant so SQLite does this as a metadata-only schema rewrite. * **`cmd/ingestor`**: `extractObserverMeta` accepts `repeat` as bool, case-insensitive string (`on|off|true|false|yes|no`), or numeric `0|1`. Missing field → `nil` → `COALESCE` preserves the existing column value (back-compat with legacy observers). Plumbed through `UpsertObserverAt` and the prepared upsert statement. * **`cmd/server`**: `GetNonRelayObserverPubkeys` + new `prefixMap.markNonRelay` drop matching candidates inside `pm.resolveWithContext` at the top of the resolver, so all 4 tiers see the pruned candidate set. `ObserverResp.CanRelay` is surfaced on `/api/observers` and `/api/observers/{id}`. `GetNodeHealth` enriches per-observer rows with `can_relay` so the node-detail badge renders. Probe-and-fall-back when the `can_relay` column is absent (legacy test fixtures). * **`public/`**: listener vs repeater pill on observers list, observer detail `Relay` stat card, and node-detail `Heard By` table. CSS uses existing theme vars. ## Test Added `TestResolveWithContext_ExcludesNonRelayObservers_Issue1290` in `cmd/server/resolve_non_relay_1290_test.go` covering all three required cases: * `repeat:off` pubkey → not a candidate (assertion failed in red commit `5f7fdb96`, passes after green `f12911dc`) * `repeat:on` pubkey → still a candidate (regression guard) * legacy obs (no field) → still a candidate (back-compat) Red→green proof: ``` $ git log --oneline origin/master..HEAD |
||
|
|
16c7ea4b82 |
fix(#1528): theme-track .vcr-scope-btn.active + .copy-link-btn:hover backgrounds (#1578)
Red commit: b018a752e82723b076316693df3271dbfb5e608b Fixes #1528 ## What Completes the four-surface accent-token migration from the triage on #1528. PR #1530 handled three of the four call-out surfaces (`.field-table .section-row td`, `.copy-link-btn` base rule, `.multibyte-badge`). This PR finishes the remaining two surfaces that still had hardcoded blue `rgba(59,130,246,...)` literals on their tinted backgrounds: - `public/live.css:1045` `.vcr-scope-btn.active` — `background` + `border-color` now go through `var(--accent-bg)` / `var(--accent-border)` with the prior literals retained as safe fallbacks. - `public/style.css:2673` `.copy-link-btn:hover` — `background` now goes through `var(--accent-border)`. ## Why The triage's "CSS-var theming illusion" finding: foreground text on these surfaces was already bound to themable tokens, but the backgrounds were blue-locked. Picking a non-blue accent in the customizer produced surfaces where the foreground tracked the theme but the background stayed blue — failing WCAG-AA on light accents (the bug screenshots in the issue). ## TDD - Red commit (`b018a752`): adds a Playwright E2E assertion that overrides `--accent-bg` / `--accent-border` on `:root` with sentinel colors and asserts `.vcr-scope-btn.active`'s computed `backgroundColor` / `borderColor` reflect them. Verified failing against the unfixed CSS — actual bg was `rgba(59, 130, 246, 0.2)`, sentinel was ignored. - Green commit (`d46055cd`): the two-line token swap. Verified passing after `docker cp` of the patched CSS onto staging — bg followed the override. E2E assertion added: `test-e2e-playwright.js:3318` ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all 9 hard gates pass, no warnings. Critically the "CSS self-fallback" and "CSS-var defined" checks (the gates that exist for exactly this class of bug) both pass. ## Scope Strictly the two remaining surfaces from #1528's fix path. No other `--accent` usage was touched. --------- Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> |
||
|
|
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" /> |
||
|
|
5fd8900cfc |
feat(packets): add Path symbols legend disclosure (closes #1504) (#1570)
## Summary Closes #1504. Adds a tiny, dismissible "Path symbols" legend next to the Path column header on the Packets page (and reused on the Nodes page's "Paths Through This Node" card), explaining the three otherwise-undiscoverable path glyphs: - `⚠N` — regional conflict count (multiple candidates for the hop's prefix in this region) - `⚠️` — unreliable name resolution (best-guess pubkey couldn't be confirmed) - dashed underline — ambiguous / global-fallback resolution ## Rationale (from triage) - **Tufte**: integrate words and graphics. A hidden per-row tooltip violates "don't make the viewer cross-reference." A small, persistent inline key next to the column header is dense, on-data, and dismissible. - **Avoid a modal** — chartjunk for a 3-glyph vocabulary. - **Munger** rejected the reporter's option #2 (hover overlay that pauses live updates): a power-user table must not stall from accidental hovers. - Single shared constant on `HopDisplay` so the Nodes page reuses the same vocabulary without drift. ## Files - `public/hop-display.js` — export `PATH_SYMBOLS_LEGEND` constant + `renderPathSymbolsLegend()` helper (no changes to existing badge rendering logic) - `public/packets.js` — wire renderer into the Path `<th>` header - `public/nodes.js` — reuse renderer on `#fullPathsSection` h4 - `public/style.css` — minimal styling (subtle dotted-underline trigger + floating disclosure panel, all via theme vars) - `test-frontend-helpers.js` — 5 new assertions (TDD red→green) ## TDD red → green - RED commit `46741267` — adds 5 assertion-shaped tests; all fail on the assertion (not on import/build). - GREEN commit `fab27ec5` — implements the constant, renderer, wiring, and CSS; all 607 frontend-helper tests pass. ## Tested via - DOM-grep assertions on the rendered `<details>` markup (`<summary>Path symbols</summary>`, all three glyphs present, dashed-underline description). - Static grep that `packets.js` invokes the shared renderer adjacent to the Path column. - Full `test-frontend-helpers.js`, `test-packet-filter.js`, `test-aging.js` pass. ## Hard rules honored - No modal, no pause-on-hover, no changes to `hop-display.js`'s badge rendering logic. - No `<img>`/SVG additions, no new CSS vars (uses existing theme vars), no Go changes. - PII grep clean on every commit and on this body. Browser verified: manual smoke pending — disclosure is closed-by-default and uses standard `<details>` semantics; renders inline with column header. E2E assertion added: `test-frontend-helpers.js` — `#1504: renderPathSymbolsLegend returns <details> disclosure with "Path symbols" summary + all glyphs` (and 4 sibling assertions). --------- Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> Co-authored-by: clawbot <bot@openclaw.local> |
||
|
|
a7ad2be142 |
fix(observers): show "Last updated" timestamp on aggregate header (closes #1562) (#1563)
Closes #1562. Follow-up to #1551 and #1552. ## Problem On CDN-fronted deployments (e.g. meshcore.meshat.se), the observers page header rendered totals computed entirely client-side from a possibly-stale `/api/observers` response. Operators saw e.g. `0 Online / 43 Stale / 37 Offline` while a cache-busted request returned `44 Online / 0 Stale / 36 Offline` — the aggregate row was the first thing they looked at to assess mesh health, so wrong numbers meant wrong actions. #1551 added `Cache-Control: no-store` on `/api/*` responses, but the client also has its own in-memory cache (`api(path, { ttl })`), and there was no UI signal at all that the rendered counts could be stale. ## Fix scope (Option 3 + light Option 2) Per the issue's three options, this PR implements **Option 3** (timestamp label) and a light **Option 2** (manual-refresh button bypasses client cache). Option 1 (a new server-side `/api/observers/summary` endpoint) is **deferred** as a follow-up — it's the most correct fix, but a bigger lift than what's needed to stop operators from acting on silently-wrong numbers. ## Changes - **`public/observers.js`** - New `window.ObserversSummary` pure helper exposing `computeCounts(observers)` and `renderHeader(counts, fetchedAt)`. Pure functions = easy to unit test. - Track `_fetchedAt` (ms) on each successful `loadObservers()` response. - `render()` delegates header HTML to `ObserversSummary.renderHeader(counts, fetchedAt)`. Existing aggregate display (`Online / Stale / Offline / Total`) is preserved exactly — the only visible additions are the "Last updated: Xs ago" label and a warning class when the timestamp is >60s old. - Manual refresh button now passes `{ bust: true }` to `api()` so the operator can force a fresh fetch when they suspect staleness. - **`public/style.css`** - New `.obs-updated` and `.obs-updated-stale` rules using existing `--text-muted` / `--warning` CSS variables (no new colors). - **`test-issue-1562-observers-summary.js`** + **`.github/workflows/deploy.yml`** - Unit tests for `computeCounts` (mixed ages → 1/1/1 + total), `renderHeader` (label presence + stale-warning class), plus DOM-grep checks that observers.js still tracks `_fetchedAt` and bypasses the cache on manual refresh. ## TDD Red commit asserts `ObserversSummary` doesn't exist / no `_fetchedAt` tracking / no `obs-updated-stale` CSS → fails. Green commit adds the implementation → passes. ## What this PR does NOT touch - **Observer health thresholds** — owned by #1552, untouched here. - **`healthStatus()` per-row classification** — untouched. The same function still gates per-row colors AND aggregate counts; the fix is about freshness visibility, not classification logic. - **No new server endpoint** — Option 1 deferred. Will file a follow-up if anyone wants that tracked. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: mc-bot <bot@meshcore.local> |
||
|
|
d7cd9203ca |
Fixes #1165: add OSM/Stamen tile providers with per-provider Leaflet layer control. (#1533)
List of changes too long to describe, so I'll hit high level. - Config now supports the json map tiles that were suggested by @Kpa-clawbot. - Leaflet map layer button appears in the top right of live.js and map.js (because all the work was already done on live.js... Added bonus) - Allows users to enter creds for OSM and Stamen to get enterprise related perks, in the config file - Added a default light map under customizer. Still suggest removing them all together and relying on the config - You can enable OSM and Stamen in the config without a license, but at your own risk!!! - Config comment explains where to register and the providers for osm, as well as the general limits per X interval - Updated tests (28) to address the changes made to the maps ### TDD Exemption **Reason**: Net-new UI surfaces (per `AGENTS.md`) This PR introduces a net-new UI surface (the multi-provider map tile selector). Under the `AGENTS.md` exemption for net-new UI surfaces, the absence of an initial failing (red) commit is permitted, as the UI was built first. However, the underlying public APIs are fully covered. The following tests serve as the first assertions for these new APIs: - `window.MC_createLayerControl`: Asserted in `MC_createLayerControl handles Auto mode and explicit layers correctly` - `window.MC_setDarkTileProvider` & `window.MC_getDarkTileProvider`: Asserted in `MC_setDarkTileProvider persists to localStorage...` - `window.MC_setLightTileProvider` & `window.MC_getLightTileProvider`: Asserted in `MC_setLightTileProvider persists to localStorage...` - `window.MC_initTileRegistry`: Asserted in `MC_initTileRegistry(true) dispatches mc-tile-provider-changed` - `applyTileFilter`: Asserted in `applyTileFilter sets invert CSS for inverted dark provider...` - Cross-tab synchronization: Asserted in `Cross-tab storage event re-dispatches mc-tile-provider-changed` |
||
|
|
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).
|
||
|
|
24a840d199 |
fix(nodes): align --card-bg with --surface-2 in dark mode — low-contrast card fix (#1470) (#1517)
## Problem In dark mode, `.node-full-card` and `.node-stats-table` (and all other `var(--card-bg)` consumers) rendered with a background only ~11 RGB units away from the page background: - Page bg: `--surface-0` = `#0f0f23` (RGB 15,15,35) - Card bg: `--surface-1` = `#1a1a2e` (RGB 26,26,46) - Delta: ~11 units per channel → appears near-white on OLED/high-contrast LCD screens ## Fix Align `--card-bg` to `--surface-2` (`#232340`) in dark mode — the same value already used for `--detail-bg` throughout the app. Delta from page bg increases to ~35 units per channel, which reads clearly as an elevated dark surface rather than a washed-out off-white card. Both dark-mode variable blocks updated in sync (`@media prefers-color-scheme: dark` + `[data-theme="dark"]`). Light mode is unchanged. ## Impact All `var(--card-bg)` consumers in dark mode get the corrected colour: node full cards, stats tables, analytics cards, packet detail panels, dropdowns, etc. The value now matches `--detail-bg` so cards and detail panels use a consistent surface colour. Closes #1470. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |