mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-10 21:27:12 +00:00
master
139 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
03b5d3fe28 |
fix(#1065): first-visit gesture discoverability hints (#1186)
Red commit:
|
||
|
|
b4f186af19 |
fix(#1062): gesture system — swipe rows, tabs, slide-over dismiss (#1185)
Red commit:
|
||
|
|
9b9848611b |
fix(#1064): edge-swipe nav drawer (Option A, wide-only) (#1184)
Red commit:
|
||
|
|
9d1f5d2395 |
fix(#1061): bottom navigation for narrow viewports (#1174)
Red commit:
|
||
|
|
05876b3a59 |
fix(#1173): replace #liveDot with packet-driven brand-logo node-pulse (#1177)
Red commit: PENDING (will update) Fixes #1173. Replaces the `#liveDot` WebSocket-connected indicator with a packet-driven node-pulse animation on the brand logo's two inner circles. ## Behavior (locked per issue spec) - **Animation curve:** `ease-out` (default per open-question 1). - **Rate cap:** 15/sec (66ms gap; default per open-question 2). Excess triggers are dropped, never queued. - **Direction:** alternates A→B / B→A across messages (aesthetic, not semantic). - **Idle ≥10s:** logo at full brightness, no animation. - **Disconnected:** `.logo-disconnected` applies `filter: grayscale(0.6) opacity(0.7)`. - **`prefers-reduced-motion: reduce`:** single-step `.logo-pulse-blip` on destination only. ## Implementation - WS handler hook lives in `public/app.js` `connectWS()` (`ws.onmessage` triggers `Logo.pulse()`; `ws.onopen`/`ws.onclose` toggle `Logo.setConnected()`). - `Logo` is a small IIFE in `app.js` that exposes `window.__corescopeLogo` for E2E injection. - All animation is pure CSS; JS only toggles `.logo-pulse-active` / `.logo-pulse-blip` / `.logo-disconnected`. Colors come exclusively from `--logo-accent` / `--logo-accent-hi` tokens. - Two new classes (`.logo-node-a`, `.logo-node-b`) attached to inner circles in both `.brand-logo` and `.brand-mark-only` SVGs so the mobile mark animates too. ## `#liveDot` removal proof ``` $ grep -rn liveDot public/ (no output) ``` ## E2E - E2E assertion added: `test-logo-pulse-1173-e2e.js:54` and follows. - Wired into the Playwright matrix in `.github/workflows/deploy.yml` (mirrors PR #1168 pattern from commit `5442652`). - Test injects synthetic pings via `window.__corescopeLogo.pulse({ synthetic: true })`; matches the existing harness style (no new WS-mock pattern invented). Red→green discipline preserved: the test commit lands first and CI fails on assertion; the implementation commit follows. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
16c48e73b3 |
fix(live): compact header + pinned controls with narrow-viewport collapse (#1178, #1179) (#1180)
Red commit:
|
||
|
|
9774403fa4 |
fix(#1058): analytics chart containers — fluid + auto-stacking (#1175)
Red commit:
|
||
|
|
89d644dd72 |
fix(#1056): row-detail slide-over panel at narrow widths (AC #4) (#1168)
Red commit:
|
||
|
|
cf604ca788 |
fix(nav): #1109 mobile hamburger dropdown clipped by .top-nav overflow:hidden (#1163)
# Fix #1109 — mobile hamburger dropdown clipped invisible by `.top-nav { overflow:hidden }` Red commit: `5429b0f` (failing E2E, asserts pixel-level visibility). ## Symptom On <768px viewports, tapping `#hamburger` toggles `.nav-links.open` and `body.nav-open` correctly — DOM state is right, `aria-expanded="true"`, computed `display:flex` — but **nothing appears below the navbar**. The dropdown is laid out at `y=52..626` but visually clipped. ## Root cause `.top-nav` is `position:sticky; height:52px; overflow:hidden` (added in #1066 fluid scaffolding at `417b460` to guard against horizontal overflow during the Priority+ measurement pass). At <768px the dropdown becomes `position:absolute; top:52px`, so its containing block is `.top-nav` — and `.top-nav`'s `overflow:hidden` clips everything below `y=52`. Result: the dropdown renders inside a 52px box and the user sees nothing. Full RCA + screenshots: https://github.com/Kpa-clawbot/CoreScope/issues/1109#issuecomment-4398900387 ## Fix In `public/style.css`, inside `@media (max-width: 767px)`, change `.nav-links` from `position:absolute` to `position:fixed`. `position:fixed` escapes any `overflow:hidden` ancestor (its containing block becomes the viewport), so the dropdown is no longer clipped. All other rules (display/flex/background/padding/z-index) keep working. This deliberately does **not** relax `overflow:hidden` on `.top-nav` — that would reopen the #1066 horizontal-overflow regression on desktop. ## Why prior tests missed this Existing nav E2Es asserted `.classList.contains('open')` / `getComputedStyle().display === 'flex'` — pure DOM state. Those passed even while the dropdown was clipped invisibly. The new test in this PR asserts **pixel-level visibility**: `document.elementFromPoint(viewportWidth/2, 100)` must land on something inside `.nav-links` (not `<body>`), and the first `.nav-link`'s bounding rect must satisfy `bottom > 60` and have non-zero area. A state-only fix can never satisfy this. E2E assertion added: `test-issue-1109-hamburger-dropdown-visible-e2e.js:113` (the `hitInsideNavLinks` check). ## Files changed - `public/style.css` — one line in the mobile media query: `position: absolute` → `position: fixed` - `test-issue-1109-hamburger-dropdown-visible-e2e.js` — new E2E - `.github/workflows/deploy.yml` — wire the new E2E into the suite Fixes #1109 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
d6256c4f94 |
fix(#1151): drop orphan separators from side-panel Heard By rows (#1161)
Fixes #1151 ## Problem The side-panel "Heard By" row template in `public/nodes.js` (line 1337) built its stats suffix with inline ternaries: ```js ${o.packetCount} pkts · ${o.avgSnr != null ? '...' : ''}${o.avgRssi != null ? ' · RSSI ...' : ''} ``` When `avgSnr` and/or `avgRssi` were `null` (very common in prod — many CJS observers have both null), this produced orphan separators: - both null → `"110 pkts · "` (trailing dot) - snr null only → `"55 pkts · · RSSI -50"` (double dot) ## Fix Build a filtered parts array, then `.join(' · ')`. Only present fields contribute, so the separator can never appear next to nothing. ```js const stats = [`${o.packetCount} pkts`]; if (o.avgSnr != null) stats.push('SNR ' + Number(o.avgSnr).toFixed(1) + 'dB'); if (o.avgRssi != null) stats.push('RSSI ' + Number(o.avgRssi).toFixed(0)); // → stats.join(' · ') ``` Full-page table (line 1337's neighbor) was already null-safe (separate `<td>` cells), so only the side-panel template needed the change. ## TDD Red commit: `1c02ff9a7889aadd16f87f4e673287f9742d4ad0` — adds `test-issue-1151-orphan-separators-e2e.js` to the deploy.yml E2E job. The test stubs `/api/nodes/:pubkey/health` via Playwright `page.route()` with four observer permutations (both null, snr-only-null, rssi-only-null, both set), opens the side panel, and asserts no `.observer-row` stat suffix matches `· ·`, leading `·`, or trailing `·`. E2E assertion added: `test-issue-1151-orphan-separators-e2e.js:96` ## Preflight All hard gates pass — see preflight output in the implementation log. --------- Co-authored-by: CoreScope Bot <bot@corescope> |
||
|
|
cfd1903c6b |
fix(logo): default sage/teal brand colors, customizer mirrors accent (#1162)
## Summary Restores sage/teal as default logo colors while preserving customizer theming. Closes the gap from #1157 (closed) and the user's complaint about lost two-tone. Out-of-the-box, the navbar + hero CORE/SCOPE wordmarks now render the brand-identity duotone — `#cfd9c9` (sage / fog) and `#2c8c8c` (teal / water). When an operator picks a theme via the customizer (or sets a custom accent color), the wordmark recolors to follow. ## Approach (Option C — decoupled defaults + customizer mirror) - **`public/style.css` `:root`** — set `--logo-accent: #cfd9c9` and `--logo-accent-hi: #2c8c8c` as literal defaults. Removes the previous `var(--accent)` cascade so blue-by-default no longer leaks into the brand mark. - **`public/customize-v2.js`** — `applyTheme()`, the early-apply path, and the live color-picker `input` handler now mirror `themeSection.accent` → `--logo-accent` and `themeSection.accentHover` → `--logo-accent-hi`. - **`public/customize.js`** (legacy) — same mirroring in `applyThemePreview()` and the early localStorage replay. - **`.github/workflows/deploy.yml`** — adds the new e2e to the Chromium batch. This preserves `--accent` as the canonical app-wide accent token (no other UI changes) while giving the logo its own brand-defaulted tokens that the customizer still drives. ## Tests Red → green commit pair on the branch. - **NEW: `test-logo-default-sage-teal-e2e.js`** — gates both halves of the contract: 1. Clean localStorage → navbar + hero CORE = `rgb(207, 217, 201)`, SCOPE = `rgb(44, 140, 140)`. 2. Seeded `cs-theme-overrides` with red accent → navbar + hero recolor to red. - **UPDATED: `test-logo-theme-e2e.js`** — replaces the old "must NOT be sage" sentinel (sage was a regression marker; it's now the brand default) with a theme-reactivity probe that overrides `--logo-accent` / `--logo-accent-hi` directly and asserts the wordmark fill changes. Duotone, mobile-fit, and clip checks are unchanged. ## Verification - Default load: sage CORE + teal SCOPE in navbar AND hero ✔ (asserted by step 1 of the new e2e). - Customizer override: wordmark follows `accent` / `accentHover` ✔ (asserted by step 2 of the new e2e + the theme-reactivity probe in `test-logo-theme-e2e.js`). - Preflight: all hard gates green (PII, branch scope, red commit, CSS-var defined, CSS self-fallback, LIKE-on-JSON, sync migration); all warnings green. ## Browser verified E2E assertion added: `test-logo-default-sage-teal-e2e.js:73` (default sage), `test-logo-default-sage-teal-e2e.js:124` (customizer override). CI runs both via `deploy.yml:243`. Browser verified: covered by Chromium e2e against `http://localhost:13581` in CI; staging URL TBD on merge. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
81f95aaabe |
fix(nav): floor More menu at >=2 items (#1139 Bug B) (#1148)
Partial fix for #1139 — closes Bug B (desktop More menu degenerate). Bug A (mobile hamburger) blocked on user device info; left for separate PR. ## What this changes `public/app.js` `applyNavPriority()` (the >1100px measurement branch): add a "minimum More menu size" floor. After the greedy `fits()` loop terminates, if exactly one link ended up in `is-overflow`, promote one more from the overflow queue so the dropdown contains ≥2 items. ```diff let i = 0; while (!fits() && i < overflowQueue.length) { overflowQueue[i].classList.add('is-overflow'); i++; } + // #1139 Bug B: floor the More menu at >=2 items. + var overflowedCount = allLinks.filter(a => a.classList.contains('is-overflow')).length; + if (overflowedCount === 1 && i < overflowQueue.length) { + overflowQueue[i].classList.add('is-overflow'); + i++; + } rebuildMoreMenu(); ``` The ≤1100px Priority+ design contract (5 high-priority + More) is unchanged; the floor only applies on the measurement branch. ## Why Above 1100px the measurement loop greedily fills inline links until something overflows. If exactly one non-priority link is wider than the remaining slack, the loop pushes only it into overflow and stops — producing a one-item "More ▾" dropdown. With the fixture stats this reproduces deterministically at 1600px (overflow=`["🎵 Lab"]`); the prod report on 1101–1278px is the same root cause with realistic `#navStats` width consuming most of the remaining slack. ## TDD - Red: `test-nav-more-floor-1139-e2e.js` sweeps 1101, 1150, 1200, 1240, 1278, 1280, 1340, 1500, 1600, 1700px and asserts `#navMoreMenu.children.length` is 0 or ≥2 — never 1. On master it fails at 1600px (`items=1, overflow=[#/audio-lab]`). - Green: with the floor in place all 10 viewports pass. - Existing `test-nav-priority-1102-e2e.js` and `test-nav-fluid-1055-e2e.js` still pass (5/5 and 20/20). - Wired into CI alongside the other nav E2E tests. ## Out of scope (Bug A) The mobile hamburger inert-button report needs a console snapshot from the affected device (pasted in the issue body) to pin the root cause. Left open for a follow-up PR. This PR uses "Partial fix" intentionally and does NOT include `Fixes #1139` so the issue stays open. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> |
||
|
|
f27132e44e |
ui(node-detail): re-order sections so Recent Packets appears before Paths (#1147) (#1160)
Fixes #1147 ## What Re-orders the node-detail sections in **both** the side panel and the full node detail page. New sequence matches operator mental order (identity → what this node SAID → who heard it → relay topology → meta): 1. Identity (name, role, badges) 2. Map + QR (full page) / Public key (side panel) 3. Overview (Last Heard, First Seen, Total Packets, etc.) 4. **Recent Packets** ← lifted from bottom 5. Heard By (observers) 6. Neighbors 7. Paths Through This Node 8. Clock Skew (hidden until populated) ## Why "What did this node originate?" is the most-asked operator question at the node-detail surface. Previously Recent Packets was the LAST section in both views — operators had to scroll past Clock Skew, Heard By, Neighbors, and Paths just to see the node's own activity. Section B4 of the node-analytics review flagged this as P1. ## Changes - `public/nodes.js`: pure template re-order in two render paths (full-page `loadFullNode`, side-panel `renderDetail`). No data, styling, or behavior changes — same DOM ids, same CSS classes, same content per section. - `test-issue-1147-section-order-e2e.js`: new Playwright test that loads a node detail page (and the side panel) against the fixture DB and asserts `Recent Packets` index in DOM order is **before** `Paths Through This Node`, `Heard By`, and `Neighbors` for both surfaces. - `.github/workflows/deploy.yml`: wired the new E2E into the existing `e2e-test` job. ## TDD trail - Red commit: `c0829fd` — adds failing E2E (Recent Packets is last). - Green commit: `29cdb22` — re-orders the templates, test passes. ## Browser verified E2E assertion added: `test-issue-1147-section-order-e2e.js:84` (full page) and `:115` (side panel). Local Chromium can't run on this host (libc reloc), so verification is via CI; server-side `grep` of rendered `/nodes.js` confirms the new section order in both code paths. ## Preflight All hard gates pass (PII, branch scope, red commit, CSS vars, self-fallback, LIKE-on-JSON, sync migration). All warning gates pass. --------- Co-authored-by: kpaclawbot <bot@kpaclawbot.local> |
||
|
|
051c251e7f |
fix(#1146): WCAG AA contrast for Paths Through This Node links in dark mode (#1159)
Red commit: a4ec258fb82f72b8d5da64492dfe9a5ff4241886 (CI run linked from
`gh pr checks` once it starts)
## Problem
"Paths Through This Node" entries in node detail (side panel
#pathsContent and full-screen #fullPathsContent) render as `<div>`
blocks, not tables. The existing rule
```css
.node-detail-section .data-table td a,
.node-full-card .data-table td a { color: var(--accent); }
```
(public/style.css:1231) only covers `<td a>`, so path-hop links inherit
UA-default `rgb(0,0,238)`. On dark theme that's ~1.8–3.0:1 against
`--card-bg: #1a1a2e` — well under the 4.5:1 WCAG AA body-text floor.
## Fix
Add an explicit rule scoped to `#pathsContent` / `#fullPathsContent`
that uses `var(--accent)` (matching the data-table pattern) plus a
`:hover` to `var(--accent-hover)`. Tracks active theme + customizer
overrides — no hard-coded colours.
After: contrast measured at **6.19:1** in dark mode (link
`rgb(74,158,255)` on `rgb(26,26,46)`).
## TDD
- **Red commit** (`a4ec258`): adds
`test-issue-1146-path-link-contrast-e2e.js` + wires it into the e2e-test
job. Loads a node detail page, mocks `/paths`, forces `data-theme=dark`,
computes WCAG luminance/contrast on the path-hop `<a>`, asserts ≥ 4.5:1.
Reverting only the CSS commit restores the failure.
- **Green commit** (`5ad20fe`): the CSS fix.
E2E assertion added: `test-issue-1146-path-link-contrast-e2e.js:120`
Browser verified: local fixture run on `http://localhost:13591` (build
of `cmd/server` with this branch's `public/`) — 3 passed, 0 failed.
## Files changed
- `public/style.css` (+14 lines, scoped CSS rule + comment)
- `test-issue-1146-path-link-contrast-e2e.js` (new, +132 lines)
- `.github/workflows/deploy.yml` (+1 line, register the new E2E)
## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ all gates pass, no warnings.
Fixes #1146
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
844536a4cc |
fix(#1150): render error state on full-page node detail when API 404s (#1158)
Red commit: |
||
|
|
eddca7acde |
fix(live): region filter wipes feed — parse {observers:[...]} response (#1136) (#1140)
## Summary Fixes #1136. The live page region filter wiped all packets, polylines, and feed entries the moment any region was selected. Root cause: `public/live.js` parsed `/api/observers` as a top-level array, but the endpoint returns `{observers:[...], server_time:"..."}` — so `observerIataMap` stayed empty and `packetMatchesRegion` rejected every packet. This was a regression introduced in #1080 (live region filter) after the typed-struct refactor wrapped the observer list in `ObserverListResponse` (cmd/server/types.go). ## Fix - Extracted the parse into `buildObserverIataMap(data)` — a pure helper that accepts both the real `{observers:[...]}` shape and a bare array (defensive). Skips observers with no IATA so the result is a direct lookup map. - `initLiveRegionFilter` now uses the helper, so the map is populated on first paint. - Exposed `_liveBuildObserverIataMap` and `_liveGetObserverIataMap` on `window` for tests (read-only — no behavior change). Backend untouched — the API shape is correct. ## Tests (red → green) **Red commit** (`test(live): failing tests for #1136 region filter wipes feed`): - `test-issue-1136-observer-iata-map.js` — failed at "helper must be exposed" assertion (parser was inlined, not extracted). - `test-issue-1136-live-region-e2e.js` — Playwright. Loads `/#/live`, queries `/api/observers` to discover an SJC observer, asserts the live module's `observerIataMap` is populated, selects SJC via `RegionFilter.setSelected`, pushes a fixture packet through `_liveBufferPacket`, and asserts a `.live-feed-item[data-hash=...]` renders. Failed at both the "map populated" and "feed renders" assertions — exactly the user-reported symptom. - Both wired into `.github/workflows/deploy.yml` (unit step + Playwright step). **Green commit** (`fix(live): parse {observers:[...]} ...`): all five unit assertions + all five E2E assertions pass. Existing `test-live-region-filter.js` from #1080 still passes (no behavior change to `packetMatchesRegion`). ## Verification (local) ``` node test-issue-1136-observer-iata-map.js # 5/5 pass node test-live-region-filter.js # 9/9 pass (regression guard) BASE_URL=http://localhost:13581 \ CHROMIUM_PATH=/usr/bin/chromium \ node test-issue-1136-live-region-e2e.js # 5/5 pass against fixture DB ``` ## Scope - One frontend file changed (`public/live.js`). - Two new tests + 2 lines of CI wiring. - No backend changes. - No refactor of unrelated `live.js` code. - Out of scope: #1108 (the related "hide nodes not seen by region" feature request) is intentionally not addressed here. Fixes #1136 --------- Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
494d3022f9 |
Partial fix for #1128: close audit gaps (z-scale, css-var lint, multi-viewport E2E, Bug 1+5 polish) (#1133)
## Partial fix for #1128 — closes the gaps PR #1131 left behind PR #1131 was a partial fix for the packets-page layout chaos (merged 2026-05-06 ~01:55 UTC, then the issue was reopened by the maintainer). #1131 shipped Bug 4 (`--surface` definition), the `.path-popover` flip + lower z-index, the debounced re-measure for Bug 1, the `.filter-bar` row-gap + `.multi-select-trigger` truncation for Bug 3, the new z-index TOKENS, and a single-viewport E2E with five individual-component assertions. This PR closes everything else the issue body and the `specs/packets-layout-audit.md` audit asked for. ### What changed (per gap) **Gap A — apply the z-index scale (audit Section 2)** #1131 added `--z-dropdown` / `--z-popover` / `--z-modal` / `--z-tooltip` but explicitly left existing literal values in place. This PR renumbers the 7 dropdowns/popovers the audit named: | Selector | Before | After | |---|---:|---:| | `.col-toggle-menu` | 50 | `var(--z-dropdown)` (100) | | `.multi-select-menu` | 90 | `var(--z-dropdown)` | | `.region-dropdown-menu` | 90 | `var(--z-dropdown)` | | `.node-filter-dropdown` | 100 | `var(--z-dropdown)` | | `.fux-saved-menu` | `var(--z-tooltip)` (9200) | `var(--z-dropdown)` | | `.fux-ac-dropdown` | `var(--z-tooltip)` | `var(--z-dropdown)` | | `.hop-conflict-popover` | `var(--z-tooltip)` | `var(--z-popover)` (300) | `.fux-ctx-menu` deliberately retains the tooltip band — context menus must float above all toolbar UI. `.region-filter-options-menu` no longer exists in the source (was renamed `.region-dropdown-menu`). The `style.css` doc-block at the top is rewritten to record the applied scale and to point operators at the new lint. **Gap B — CSS-var lint (audit Section 5 #1, "single highest-value addition")** Adds `scripts/check-css-vars.js` (~70 lines). Walks `public/*.css`, extracts every `var(--name)` reference WITHOUT a fallback, asserts the name is defined in some `public/*.css`. References WITH a fallback are tolerated. Wired into CI in the `go-test` job before the JS unit tests. The red commit (`608d81f`) shipped this lint exiting 1 against the master tree — three undefined vars that bypassed earlier review: ``` public/style.css:2628 var(--text-primary) public/style.css:2675 var(--bg-hover) public/style.css:2924 var(--primary) ``` The green commit (`1369d1e`) defines those three as aliases in the :root block (`--text-primary` → `--text`, `--bg-hover` → `--hover-bg`, `--primary` → `--accent`). Light + dark themes inherit through the existing tokens. **Gap C — multi-viewport E2E (issue acceptance criterion)** Adds `test-issue-1128-multi-viewport-e2e.js` — sister of the existing single-viewport test. At each of three viewports (1280×900, 1080×800, 768×1024): - takes a screenshot to `e2e-screenshots/issue-1128-<viewport>.png` - asserts no two `.filter-group` siblings vertically overlap - on desktop+laptop, opens the Saved menu and the Types multi-select and asserts the dropdown does not vertically overlap any `.filter-group` below it Plus three viewport-agnostic assertions: - dropdown selectors compute z-index in `[100,199]` (`.col-toggle-menu`, `.multi-select-menu`, `.region-filter-options-menu`, `.fux-saved-menu`, `.fux-ac-dropdown`) - `.path-hops .hop / .hop-named / .arrow` compute `line-height ≤ 18px` - `.col-path` computes `height ≤ 28px` Wired into the e2e-test job after the existing #1128 test. **Gap D — Bug 5 polish (toolbar reorder)** Audit Section 3 Bug 5: swaps `filter-group-dropdowns` and `filter-group-toggles` in `public/packets.js` so time range + Group by Hash + ★ My Nodes sit next to the search input. Pure markup reorder. No CSS / no JS-handler changes. **Gap E — Bug 1 belt-and-suspenders** Audit Section 3 Bug 1 sub-bullets: - locks `.path-hops .hop / .hop-named / .arrow` to `line-height: 18px` so a chip with mixed font metrics cannot overflow the 22px host vertically and bleed into the row above - converts `.col-path { max-height: 28px }` → `height: 28px` because browsers widely ignore `max-height` on `<td>`s; the earlier rule was a no-op ### TDD discipline (red → green) ``` $ git log --oneline origin/master..HEAD |
||
|
|
364c5766fc |
feat(logo): wire new CoreScope SVG logo into navbar + home hero (#1137)
## Adds new logo and home hero Replaces the navbar mushroom emoji + "CoreScope" text spans with the new CoreScope SVG mark, and adds a hero SVG (with the MESH ANALYZER tagline) above the home page H1. ### What changed - `public/img/corescope-logo.svg` — navbar mark, no tagline (locked "aggressive low-amp chirp" variant: facing-arcs + low-amp chirp connector between the two nodes). - `public/img/corescope-hero.svg` — home hero version, includes the MESH ANALYZER tagline. - `public/index.html` — replaces `<span class="brand-icon">🍄</span><span class="brand-text">CoreScope</span>` with `<img class="brand-logo" src="img/corescope-logo.svg?__BUST__" …>`. `.nav-brand` link still routes to `#/`. `.live-dot` retained. - `public/style.css` — adds `.brand-logo { height: 36px }` (32px on tablet ≤900px). Existing 52px nav height unchanged. - `public/home.js` / `public/home.css` — adds `<img class="home-hero-logo">` above the hero `<h1>`, sized `max-width: min(720px, 90vw)` and centered. ### TDD Red→green is visible in the branch: - `3159b82` — `test(logo): add failing E2E …` (red commit). Adds `test-logo-rebrand-e2e.js` and wires it into the `e2e-test` job in `deploy.yml` with `CHROMIUM_REQUIRE=1`. On this commit `index.html` still has the emoji + text spans, `home.js` has no hero img, and the SVG asset files do not exist — the test asserts on each so CI fails on assertion. - `19434e1` — `feat(logo): wire new CoreScope SVG logo …` (green commit). Implements the fix. ### E2E asserts 1. `.nav-brand img` exists with `src` ending `corescope-logo.svg` 2. legacy `.brand-icon` / `.brand-text` are gone 3. `.live-dot` is present, visible, and to the right of the logo (no overlap) 4. `.home-hero img.home-hero-logo` exists with `src` ending `corescope-hero.svg`, positioned BEFORE the `<h1>` 5. both `/img/corescope-{logo,hero}.svg` return 200 with svg content-type ### Customizer compatibility - `customize.js` still does `querySelector('.brand-text')` / `.brand-icon` for live branding updates. Both now return `null`; existing `if (el)` guards make those branches silent no-ops. **No JS errors, but the customizer's `branding.siteName` and `branding.logoUrl` fields no longer rewrite the navbar brand** — the brand is now a fixed SVG asset. - **Theme accent does NOT recolor the SVG.** SVGs loaded via `<img src>` are isolated documents and cannot inherit document CSS variables; the SVG falls back to its embedded brand colors. This is appropriate for a brand mark; if recoloring per theme is desired later, swap to inline SVG (separate PR). ### Browser validation Local Chromium not available in this env; the E2E test soft-skips locally and hard-fails in CI (`CHROMIUM_REQUIRE=1`). Server-side checks done locally: - `curl http://localhost:13581/` → confirmed `<img class="brand-logo" src="img/corescope-logo.svg?<bust>" …>` rendered, no `.brand-icon`/`.brand-text` spans. - `curl -I /img/corescope-logo.svg` and `/img/corescope-hero.svg` → both 200. ### Performance No hot-path changes. Two new static SVG assets (~7.6KB each), served directly by the Go static handler. Cache-busted via `?__BUST__` (auto-replaced server-side). --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> |
||
|
|
b03ef4abd3 |
fix(packets): resolve --surface undefined + z-index scale + path chip re-measure + +N popover (#1128) (#1131)
## Summary Resolves the **5 layout bugs** documented in `specs/packets-layout-audit.md` from the issue investigation. All fixes shipped in one PR per the audit's recommended fix order. Fixes #1128 ### Bug 4 (P0, 1 line) — `--surface` undefined `var(--surface)` was referenced in **8** rules across `style.css` (`.fux-saved-menu`, `.fux-popover`, `.path-popover`, `.fux-ac-dropdown`, `.fux-ctx-menu`, `.path-overflow-pill:hover`, `.fux-saved-trigger:hover`, `.fux-popover-header sticky`) but the variable was **never defined** — every caller resolved to `transparent` and row content bled through. Aliased `--surface: var(--surface-1);` in the `:root`, `@media (prefers-color-scheme: dark)`, and `[data-theme="dark"]` blocks. ### Z-index scale (foundational) Added documented custom properties at the top of `style.css`: ```css --z-base: 0; --z-dropdown: 100; --z-popover: 300; --z-modal-backdrop: 9000; --z-modal: 9100; --z-tooltip: 9200; ``` New code uses these tokens. Existing working values left in place to avoid behavioural risk. ### Bug 1 — path chip re-measure `_finalizePathOverflow` runs **before** `hop-resolver` mutates chip text from hex prefix → longer node name. Chips that fit on first measurement overflow once names resolve, but the `+N` pill never gets appended. Cleared the per-host `overflowChecked` guard and re-ran finalize on a 120 ms debounced timer, so post-resolution overflow is detected. ### Bug 2 — `+N` popover position + z-index `.path-popover` was `z-index: 10500` (above the modal stack) and only ever positioned **below** the pill — when near the bottom of the viewport it hung over adjacent rows. Lowered to `var(--z-popover)` (300), capped `max-height` from `60vh` → `240px`, and added flip-above logic when there isn't room below. ### Bug 3 — filter-bar gap + multi-select truncation `.filter-bar { row-gap: 6px }` was too tight for the 34px controls; bumped to `12px`. `.multi-select-trigger` had no `max-width`, so a selection like `"TRACE,MULTIPART,GRP_TXT"` ballooned the row and overlapped toolbar buttons. Capped `max-width: 180px` with `text-overflow: ellipsis` and surfaced the full selection in the trigger's `title` attribute (so the value remains discoverable). ### Bug 5 — already addressed in #1124 Verified `.filter-group` structure prevents mid-cluster wrap; no further change needed here. ## TDD Branch shows the required **red → green** sequence: | commit | result | |---|---| | `8ad6394` test(packets): red E2E for issue #1128 layout chaos | ✗ Bug 4 (alpha=0), ✗ Bug 2 (z=10500), ✗ Bug 3 (gap=6) | | `eacadc1` fix(packets): resolve --surface undefined + z-index scale + ... | ✓ 5/5 | Test file: `test-issue-1128-packets-layout-e2e.js` — asserts opaque dropdown background, every overflowing `.path-hops` has a `+N` pill, popover z-index ≤ 9000 + anchored to pill, filter-bar gap ≥ 10px, trigger `max-width` bounded. ## E2E Local run against the e2e fixture: ``` === #1128 packets layout E2E === ✓ navigate to /packets and wait for table + rows ✓ Bug 4: Saved-filter dropdown background is OPAQUE (alpha ≥ 0.99) ✓ Bug 1: every overflowing .path-hops has a .path-overflow-pill ✓ Bug 2: +N popover anchored to pill + z-index ≤ 9000 ✓ Bug 3: .filter-bar row-gap ≥ 10px AND .multi-select-trigger has bounded max-width === Results: passed 5 failed 0 === ``` CI hookup: please add `node test-issue-1128-packets-layout-e2e.js` alongside the other `test-issue-XXXX-*-e2e.js` invocations in `.github/workflows/deploy.yml` (line ~226). ## Files - `public/style.css` — `--surface` definition × 3 blocks, z-index scale tokens, `.path-popover`, `.filter-bar`, `.multi-select-trigger` - `public/packets.js` — flip-above popover logic, debounced re-finalize, trigger `title` - `test-issue-1128-packets-layout-e2e.js` — new E2E (red → green) --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com> |
||
|
|
f86a44d6b5 |
fix(packets): filter UX disaster — modal help, single header, bounded path rows (#1122) (#1124)
## Summary Fixes #1122 — Packets page filter UX repairs. ## Bugs addressed 1. **Filter syntax help no longer floats over the packet table.** It now opens inside a real `.modal-overlay` backdrop and is centered via the existing `.modal` flex pattern (same pattern as BYOP). 2. **Duplicate "Filter syntax" header removed.** The inner `<h3>` was redundant with the popover header `<strong>`. Help body now contains exactly one occurrence. 3. **Path column chips no longer spill into adjacent rows.** `.path-hops` now uses `flex-wrap: nowrap` + `max-height: 22px` + `overflow: hidden`; individual `.hop-named` chips cap at `max-width: 120px` with ellipsis. `td.col-path` itself caps at `max-height: 28px` so a long hop chain can never push the row past 28px regardless of hop count. 4. **Toolbar grouping documented.** Added a fenced section comment in `style.css` enumerating the four logical clusters (quick filters / toggles / time window / sort & view), bumped `.filter-bar` gap from 6→8px and added `row-gap: 6px` so wrapped controls stay readable at narrow widths. ## Test TDD red→green. New Playwright E2E `test-issue-1122-packets-filter-ux-e2e.js` asserts: - Help panel rect does not overlap any visible `#pktBody` row, and a `.modal-overlay` backdrop is present. - Help panel contains exactly 1 `Filter syntax` occurrence (not 2). - Every rendered `.col-path` cell stays under 60px height. Wired into `.github/workflows/deploy.yml` Playwright fail-fast step. Red commit: `bd58634` (test only). Green commit: `c580254` (impl). ## Files - `public/filter-ux.js` — `_showHelp` wraps the popover in `.modal-overlay`; `_buildHelpHtml` drops the duplicate `<h3>Filter syntax</h3>`. - `public/style.css` — `.modal-overlay > .fux-popover` reset, `.path-hops` clipping, `td.col-path` height cap, `.filter-bar` section comment. - `test-issue-1122-packets-filter-ux-e2e.js` — new Playwright E2E. - `.github/workflows/deploy.yml` — runs the new E2E. --------- Co-authored-by: clawbot <clawbot@example.com> Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
12d96a9d15 |
fix(#1111): hide My Channels section entirely when empty (#1112)
Fixes #1111. ## Problem When the user has no PSK channels added, `public/channels.js` still renders the "My Channels 🖥️ (this browser)" section header plus an empty-state placeholder ("No channels yet — click [+ Add Channel] to add one."). The section should not exist in the DOM at all when empty. ## Fix Wrap the entire My Channels section render in a `mine.length > 0` guard. When `mine.length === 0`: no section, no header, no placeholder. ## TDD - **Red commit** (`b8bf938`): adds `test-channel-issue-1111-e2e.js`, which fails on the current renderer because the section always emits — the test reproduces the bug. - **Green commit** (`776653d`): the conditional render in `public/channels.js` makes the test pass. ## E2E New test: `test-channel-issue-1111-e2e.js` (wired into the deploy workflow alongside the other channel E2Es). - Case 1: clear `localStorage` → asserts `.ch-section-mychannels` absent and no "My Channels" text in `#chList`. - Case 2: seed `corescope_channel_keys` with one PSK key → asserts `.ch-section-mychannels` exists with the "My Channels" header. ## Acceptance criteria - [x] No "My Channels" section when empty (no header, no placeholder) - [x] Section + header + channel row render with ≥1 stored PSK key - [x] E2E covers both states ## Performance None — single conditional around an existing render path. --------- Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com> Co-authored-by: clawbot <bot@kpabap.invalid> |
||
|
|
bf68a99acf |
polish: nav Priority+ hardening + tighter E2E (Fixes #1105) (#1106)
Fixes #1105. Polish follow-ups from #1104's independent review (https://github.com/Kpa-clawbot/CoreScope/pull/1104#issuecomment-4381850096). All 9 MINORs addressed. ## Hardening (`public/app.js`, commit |
||
|
|
4cd51a41e7 |
fix(channels): strip Share modal — remove redundant URL copy + duplicated key field (#1101) (#1103)
## Summary Strips the Share Channel modal (shipped in #1090) down to its essentials. Removes redundant affordances that the QR already provides. ## What changed **Removed from the Share modal:** - The URL text printed inside the QR box (the QR encodes the URL) - The inline Copy Key button inside the QR box (overlapped the image) - The `meshcore://` URL input field below the QR - The Copy URL button next to the URL field **Result — the modal now contains exactly:** - Title `Share: <Channel Name>` - QR code (just the QR `<img>`, nothing else in that box) - Hex Key field with a single Copy button BELOW the QR - Privacy warning - ✕ close button (top right) ## Implementation - `public/channels.js` — drop the `meshcore://` URL field-group from share modal markup; `openShareModal()` no longer looks up `#chShareUrl` or builds a URL into a field; pass `{ qrOnly: true }` when calling `ChannelQR.generate` so the QR box renders ONLY the QR image. - `public/channel-qr.js` — `generate(name, secret, target, opts)` now accepts `opts.qrOnly` which short-circuits before appending the inline URL line + Copy Key button. Default behaviour (no opts) unchanged, so the Add-Channel "Generate & Show QR" flow is untouched. ## Tests (TDD: red → green) - New: `test-channel-issue-1101.js` (static grep) — asserts the URL field is gone from markup, `openShareModal` no longer references it, and `ChannelQR.generate` honours `qrOnly`. - Updated: `test-channel-issue-1087.js` and `test-channel-issue-1087-e2e.js` — those previously asserted the URL field's presence (which is exactly what #1101 removes); they now assert ONLY the hex key field exists, AND that `#chShareQr` contains exactly one `<img>` and no `.channel-qr-url` / `.channel-qr-copy` children. - Wired into `.github/workflows/deploy.yml` `node-test` job. Commit history shows red (test commit `c0c254a`) → green (fix commit `6315a19`) per AGENTS.md TDD requirement. E2E assertion added: test-channel-issue-1087-e2e.js:184 ## Acceptance criteria - [x] Share modal contains only: QR, "Copy Key" button, privacy warning - [x] No "Copy URL" affordance anywhere in the modal - [x] No duplicated hex key field below - [x] E2E test asserts the absence of the removed elements Fixes #1101 --------- Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
52bb07d6c1 |
feat(#1056): fluid tables + +N hidden pill (packets/nodes/observers) (#1099)
## Summary Implements priority-based responsive column hiding for the three primary data tables (Packets, Nodes, Observers) per the parent task #1050 acceptance criteria, with a clickable **+N hidden** pill in the table header to reveal collapsed columns. ## Approach - New `TableResponsive` helper (defined once at the top of `packets.js`, exposed on `window`) classifies `<th data-priority="N">` cells: - `1` = always visible - `2` = hide when viewport ≤ 1280 - `3` = hide ≤ 1080 - `4` = hide ≤ 900 - `5` = hide ≤ 768 - Higher priority numbers drop first. The matching `<td>` cells in `tbody` are tagged via `.col-hidden` (colspan-aware mapping). - A `.col-hidden-pill` `<button>` is appended to the last visible `<th>`. Clicking it sets a per-table reveal flag and clears all hidden classes. Re-runs on `window.resize` (debounced) and a `ResizeObserver` on the wrapping element. - Each of `packets.js` / `nodes.js` / `observers.js` wraps its primary table in `.table-fluid-wrap` and calls `TableResponsive.register` after initial render. - `style.css` removes legacy `min-width: 720px / 480px` floors on the primary tables (which forced horizontal scroll) and lets columns flex via `table-layout: auto` with `.col-time` switched to `clamp(72px, 8vw, 108px)`. Per-column priorities chosen so identifier columns stay visible (Time/Hash/Type/Name/Status) while numeric/secondary columns collapse first. ## Files changed (matches Hard rules — only these) - `public/packets.js` (`#pktTable` + `TableResponsive` helper) - `public/nodes.js` (`#nodesTable`) - `public/observers.js` (`#obsTable`) - `public/style.css` (table sections only) - `test-table-fluid-e2e.js` (new E2E) ## E2E `BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js` — covers all three tables at 768/1080/1440 viewports, asserting: - No horizontal table overflow within `.table-fluid-wrap` - Visible `+N hidden` pill at narrow widths with the count `N` matching the number of `th.col-hidden` cells - Clicking the pill clears all `.col-hidden` classifiers (reveals every column) ## Manual verification in openclaw browser (local fixture server) | Page | Viewport | Hidden | Pill | |-----------|---------:|-------:|--------------| | observers | 768 | 8 | `+8 hidden` | | packets | 768 | 7 | `+7 hidden` | | packets | 1080 | 4 | `+4 hidden` | | nodes | 768 | 3 | `+3 hidden` | | nodes | 1440 | 0 | (no pill) | Pill click verified to reveal all columns. ## TDD - Red commit: `5ad7573` — failing E2E (no `.col-hidden-pill` exists yet) - Green commit: `7780090` — implementation; test passes manually against fixture server. Fixes #1056 --------- Co-authored-by: openclaw-bot <bot@openclaw.dev> Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
85b8c8115a |
feat(channels): fluid sidebar + container-query stacking (#1057) (#1095)
## Summary Makes the channels page sidebar + message area fluid as part of the parent #1050 fluid-layout effort. Replaces the hardcoded `.ch-sidebar { width: 280px; min-width: 280px }` with `width: clamp(220px, 22vw, 320px); min-width: 220px`. Adds an `@container` query (via `container-type: inline-size` on `.ch-layout`) that stacks the sidebar above the message area when the channels page itself is narrow (≤700px container width) — independent of the global viewport, so it adapts even when an outer panel is consuming width. Removes the legacy `@media (max-width: 900px)` fixed 220px override; the clamp + container query handle that range. `.ch-main` already used `flex: 1`, so it absorbs all remaining width including ultrawides. The existing mobile (≤640px) overlay rules and the JS resize handle in `channels.js` are untouched and still work (user drag still wins via inline width). Fixes #1057. ## Scope - `public/style.css` — channels section only - (no `public/channels.js` changes needed) ## Tests TDD: red commit (failing tests) → green commit (implementation). - `test-channel-fluid-layout.js` (new): static CSS assertions - `.ch-sidebar` uses `clamp()` for width (not fixed px) - `.ch-sidebar` keeps a sane `min-width` (200–280px) - `.ch-main` keeps `flex: 1` - `.ch-layout` declares `container-type` (container query root) - `@container` rule scopes channels stacking - legacy `@media (max-width: 900px) .ch-sidebar { width: 220px }` is gone - `test-channel-fluid-e2e.js` (new): Playwright E2E at 768 / 1080 / 1440 / 1920 (wide) and 480 (narrow). Asserts: - no horizontal scroll on the body - sidebar AND message area both visible side-by-side at ≥768px - sidebar consumes ≤45% of viewport, main ≥40% - at 480px the layout stacks (or overlays) — no overflow Wired into `test-all.sh` and the unit + e2e steps of `.github/workflows/deploy.yml`. ## Verification - Static unit test: 6/6 pass on the green commit, 4/6 fail on the red commit (only the two trivially-true assertions pass). - Local Go server boot: `corescope-server` serves the updated `style.css` containing `container-type: inline-size`, `clamp(220px, 22vw, 320px)`, and `@container chlayout (max-width: 700px)`. - Local Chromium on the dev sandbox is musl-incompatible (Playwright fallback build crashes with `Error relocating ...: posix_fallocate64: symbol not found`), so the E2E was not run locally. CI will run it on Ubuntu runners. --------- Co-authored-by: clawbot <clawbot@example.com> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
d1e6c733dc |
fix(nav): apply Priority+ at all widths (#1055) (#1097)
## Summary Make the top-nav use the **Priority+ pattern at all widths** (not just 768–1279px), so the nav-right cluster never gets pushed off-screen or visually overlapped by the link strip. Fixes #1055. ## What changed **`public/style.css`** — nav section only (clearly fenced): - Removed the upper bound on the Priority+ media query (`max-width: 1279px`). The rule now applies at any viewport `>= 768px`. Above that breakpoint, only `data-priority="high"` links render inline; the rest collapse into the existing `More ▾` overflow menu. - Swapped nav-only hardcoded spacing/type to the fluid `clamp()` tokens shipped in #1054: - `.top-nav` padding → `var(--gutter)` - `.nav-left` gap → `var(--space-lg)` - `.nav-brand` gap → `var(--space-sm)`, font-size → `var(--fs-md)` - `.nav-links` gap → `var(--space-xs)` - `.nav-link` padding → `clamp(8px, 0.6vw + 4px, 14px)`, font-size → `var(--fs-sm)` - `.nav-right` gap → `var(--space-sm)` - Mobile (<768px) hamburger layout, the More-menu markup, and the JS that builds the menu in `public/app.js` are unchanged — they already supported this pattern. `public/index.html` did not need changes — the `data-priority="high"` markup, `nav-more-wrap`, `navMoreBtn`, and `navMoreMenu` are already in place from earlier work. ## Why the bug existed The previous Priority+ rule was scoped `@media (min-width: 768px) and (max-width: 1279px)`. From 1280px–~1599px the full 11-link strip rendered but didn't fit alongside `.nav-stats` + `.nav-right`. The parent `overflow: hidden` masked the symptom, but the rightmost links physically rendered underneath `.nav-right` and were unreachable. ## E2E assertion added New `test-nav-fluid-1055-e2e.js` — Playwright multi-viewport test (768/1024/1280/1440/1920) that asserts: 1. `.nav-right.right` ≤ `document.documentElement.clientWidth` (no horizontal overflow) 2. Last visible `.nav-link.right` ≤ `.nav-right.left` (no overlap underneath the right cluster) 3. `.top-nav.scrollWidth` ≤ `.top-nav.clientWidth` (no scrolled-off content) Wired into the `e2e-test` job in `.github/workflows/deploy.yml`. **TDD evidence:** - Red commit `466221a`: test passes 3/5 (1024/768/1920) — fails at 1280 (253px overlap) and 1440 (93px overlap). - Green commit `1aa939a`: test passes 5/5. ## Acceptance criteria (from #1055) - [x] Priority+ at ALL widths (not just mobile). - [x] No nav link overflow at 1080px (or any tested width). - [x] Overflow menu accessible via keyboard + touch (existing `navMoreBtn` aria-haspopup wiring; verified by existing app.js handlers). - [x] Active route still highlighted when in overflow (existing logic in `app.js` adds `.active` to the cloned link in `navMoreMenu`). - [x] Tested at 768/1024/1280/1440/1920 — visible link count adapts (5 priority links + More menu at all desktop widths; full 11 inline only on hamburger mobile when expanded). --------- Co-authored-by: bot <bot@corescope> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
b52a938b27 |
fix(#1059): map controls + modals — fluid + safe max-height (#1096)
## Summary Fixes #1059 — Task 6 of #1050. Makes map controls + modals fluid and safely capped so they work across 768px–2560px viewports. ## Changes `public/style.css` only — modal section + map-controls section (per task scope). ### Map controls (`.map-controls`) - `width: clamp(160px, 18vw, 240px)` — fluid, scales with viewport. - `max-width: calc(100vw - 24px)` — never overflows narrow viewports. - Eliminates horizontal scroll on the map page at 768/1024/1440/1920/2560. ### Modal box (`.modal`) - `max-height: 80vh → 90vh` (spec §3). - `width: min(90vw, 500px)` — fluid, drops to 90vw below 555px. - `position: relative` so sticky descendants anchor to the modal box. - `.modal-overlay` gets `padding: clamp(8px, 2vw, 24px)` for edge breathing room. ### BYOP modal sticky close - `.byop-header { position: sticky; top: 0 }` with `var(--card-bg)` backdrop and bottom border — the title bar + ✕ stay reachable while the body scrolls. - `.byop-x` restyled with border, hit area, hover state. ### Untouched (intentional) - `public/map.js` did not need changes — the `.map-controls` element is the only narrow-viewport offender; the markup stays identical. - Channel modals (`.ch-modal*`, `.ch-share-modal*`) already have their own width/max-width tokens from #1034/#1087 and are out of scope for this task. ## TDD - **Red commit** `b69e992`: `test-map-modal-fluid-e2e.js` asserts (a) no horizontal scroll on `/#/map` at 1024/1440/1920/2560, (b) `.map-controls` right edge inside viewport at 768px wide, (c) BYOP modal at 1024×768 has `height ≤ 90vh`, `overflow-y: auto|scroll`, and close button is `position: sticky` and reachable. All assertions fail against the previous CSS (fixed-width 220px controls overflow at narrow widths; modal max-height was 80vh, not 90vh; close button was `position: static`). - **Green commit** `3e6df9d`: CSS changes above; all assertions pass. ## E2E - Wired into `.github/workflows/deploy.yml` after the channel-1087 E2E: ``` BASE_URL=http://localhost:13581 node test-map-modal-fluid-e2e.js ``` ## Acceptance criteria - [x] Map controls do not overlap markers at narrow viewports (fluid clamp width + max-width). - [x] Map fills extra space on ultrawide (panel caps at 240px, leaflet flex:1 takes the rest — already true; controls no longer steal grow room). - [x] Modals: `max-height: 90vh`, internal scroll, sticky close button, max-width via `min()`. - [x] No modal can exceed viewport height at any tested width. - [x] Verified via E2E at 768/1024/1440/1920/2560. ## Out of scope (left for sibling tasks under #1050) - Tab bars / nav (Task 1050-1, blocker). - Filter bars and table chrome (other 1050-N tasks). --------- Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
88dca33355 |
fix(touch): tune pull-to-reconnect to require deliberate pull (#1091) (#1092)
## Summary Fixes #1091 — pull-to-reconnect was triggering on normal scrolling because the threshold was too low and `preventDefault` fired too early. ## Changes **`public/app.js`** — `setupPullToReconnect()` gesture tuning: | Behavior | Before | After | |---|---|---| | Threshold | 80px | **140px** (deliberate pull, not bounce) | | `preventDefault` fires at | 16px (kills native scroll feel) | **140px** (only after commit) | | scrollTop check | `> 0` (allowed negative overscroll) | **strict `=== 0`** | | Mid-gesture scroll | continued tracking | **cancels gesture** | | `touchend` scrollTop check | none | **must still be 0** | ## TDD evidence - Red commit: `bcf0d79` — added `test-pull-to-reconnect-1091.js`. The "100px pull at scrollTop=0: NO reconnect" assertion fails on master because the old 80px threshold triggers there. Six other gesture-tuning assertions also gated. - Green commit: `4071dd0` — production fix. All 7 new tests + 6 existing pull-to-reconnect tests pass. ## E2E coverage (per acceptance criteria) - 50px pull → no trigger - 100px pull → no trigger (regression guard against old 80px threshold) - 160px pull → triggers - Pull from non-zero scrollTop → no trigger - Lift before threshold → no trigger - scrollTop changes from 0 mid-pull → cancels - preventDefault not called below threshold E2E assertion added: `test-pull-to-reconnect-1091.js:154` (the 100px regression-guard assertion that demonstrates the bug fix). ## Test results ``` test-pull-to-reconnect-1091.js: 7 passed, 0 failed test-pull-to-reconnect.js: 6 passed, 0 failed ``` Fixes #1091 --------- Co-authored-by: clawbot <bot@openclaw.local> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
ac0cf5ac7d |
fix(channels): #1087 QR library + share modal + PSK persistence (#1090)
Red commit:
|
||
|
|
282074b19d |
feat(#1034): wire QR generate + scan into channel modal (PR 3/3) (#1081)
## Summary **PR 3/3 of #1034** — wires the existing `window.ChannelQR` module (PR2 #1035) into the existing channel modal placeholders (PR1 #1037). ### Changes **`public/channels.js`** - **Generate handler** (`#chGenerateBtn`): replaced the "QR coming in next update" placeholder text with a real call to `window.ChannelQR.generate(label || channelName, keyHex, qrOut)`. Renders QR canvas + `meshcore://channel/add?...` URL + Copy Key inline into `#qr-output`. - **Scan handler** (`#scan-qr-btn`): removed `disabled` attribute, refreshed title, and added a click handler that calls `window.ChannelQR.scan()`. On success it populates `#chPskKey` (from `result.secret`) and `#chPskName` (from `result.name`); on cancel it's a no-op; on error it surfaces the message via `#chPskError`. The Share button on sidebar entries was already wired to `ChannelQR.generate` in PR1 (no change needed). ### TDD 1. **Red commit** (`178020b`): `test-channel-qr-wiring.js` — 12 assertions, 7 failed against the placeholder code (Generate handler still printed "coming in next update", scan button still disabled). 2. **Green commit** (`e708f3f`): wiring added → all 12 assertions pass. ### E2E (rule 18) `test-e2e-playwright.js` gains 3 Playwright tests (run against the live Go server with fixture DB in CI): - Generate → asserts `#qr-output canvas` and the `meshcore://channel/add` URL appear after the click. - Scan button is enabled (no `disabled` attribute). - Stubs `ChannelQR.scan` to return `{name, secret}`, clicks the button, asserts `#chPskKey` + `#chPskName` are populated. ### CI registration Added `node test-channel-qr-wiring.js` and `node test-channel-modal-ux.js` to the JS unit-test step in `.github/workflows/deploy.yml` (and `test-all.sh`). ### Closes Closes #1034 (final PR in the redesign series). --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> |
||
|
|
f7d8a7cb8f |
feat(packets): filter UX — in-UI docs + autocomplete + right-click + saved filters (#966) (#1083)
## Summary Implements the full filter-input UX upgrade from #966 — Wireshark-style help, autocomplete, right-click-to-filter, and saved filters. Closes #966. ## Surfaces ### A. Help popover (ⓘ button next to filter input) Auto-generated from `PacketFilter.FIELDS` / `OPERATORS` so it stays in sync with the parser. Includes: - Syntax overview (boolean ops, parens, case-insensitivity, URL-shareable filters) - Full field reference (27 entries: top-level + `payload.*`) - Full operator reference with one example per op - 10 ready-to-paste examples - Tips (right-click, autocomplete, save) ### B. Autocomplete dropdown - Type partial field name → field suggestions (top-level + dynamic `payload.*` keys discovered from visible packets) - Type `field` → operator suggestions - Type `type ==` → list of canonical type values (`ADVERT`, `GRP_TXT`, …) - Type `route ==` → list of route values (`FLOOD`, `DIRECT`, `TRANSPORT_FLOOD`, …) - Keyboard nav: ↑/↓, Tab/Enter to accept, Esc to dismiss ### C. Right-click → filter by this value Right-click any of these cells in the packet table: - `hash`, `size`, `type`, `observer` Context menu offers `==`, `!=`, `contains`. Click → clause appended to filter input (with `&&` if expression already present). ### D. Saved filters - ★ Saved ▾ dropdown next to the input - 7 starter defaults (Adverts only, Channel traffic, Direct messages, Strong signal SNR > 5, Multi-hop, Repeater adverts, Recent < 5m) - "+ Save current expression" prompts for a name and persists to `localStorage` under `corescope_saved_filters_v1` - User filters can be deleted (✕); defaults cannot - User filters with the same name as a default override it ## Implementation **`public/packet-filter.js`** — exposes `FIELDS`, `OPERATORS`, `TYPE_VALUES`, `ROUTE_VALUES`, and a new `suggest(input, cursor, opts)` function that returns ranked autocomplete suggestions with replace-range. Pure function — no DOM, fully unit-tested. **NEW `public/filter-ux.js`** — `window.FilterUX` IIFE owning the help popover, autocomplete dropdown, context menu, and saved-filters store. `init()` is idempotent, called once after the filter input renders. **`public/packets.js`** — calls `FilterUX.init()` after the filter input IIFE; row builders gain `data-filter-field` / `data-filter-value` attrs on hash/size/type/observer cells. `filter-group` wrapper now `position: relative` so dropdowns anchor correctly. **`public/style.css`** — scoped `.fux-*` styles using existing CSS variables (no new theme tokens). ## Tests - `test-packet-filter-ux.js` (19 unit tests, wired into `test-all.sh`): - Metadata exposure (FIELDS / OPERATORS / TYPE_VALUES / ROUTE_VALUES) - `suggest()` for empty input, prefix match, after `==`, dynamic `payload.*` keys - `SavedFilters.list/save/delete` — defaults, persistence, override, dedup - `buildCellFilterClause()` and `appendClauseToExpr()` quoting + appending - `test-filter-ux-e2e.js` (Playwright, wired into `deploy.yml`): - Navigate /packets → metadata exposed - Help popover opens with field reference, operators, examples - Autocomplete shows on focus, filters by prefix, accepts on Enter - Saved-filter dropdown lists defaults, click populates input - Right-click on TYPE cell → context menu → click appends clause - Save current expression persists to localStorage TDD red commit (`bddf1c1`) — assertion failures only, no import errors. Green commit (`0d3f381`) — all 19 unit tests pass. ## Browser validation Spawned local server on :39966 against the e2e fixture DB and exercised every UX surface via the openclaw browser tool. Confirmed: - `window.PacketFilter.FIELDS.length === 27`, `suggest()` available - `FilterUX.SavedFilters.list().length === 7` (defaults seeded) - Help popover renders with `payload.name`, `contains`, `ADVERT` text content - Right-click on a `data-filter-field="type"` / `data-filter-value="Response"` cell → context menu showed three options → clicking == populated the input with `type == "Response"` (and the existing alias resolver matched it to `payload_type === 1`) - Autocomplete on `pay` returned `payload_bytes`, `payload_hex`, `payload.name`, `payload.lat`, `payload.lon`, `payload.text` ## Out of scope (deferred per the issue) - Server-synced saved filters (cross-device) - Visual filter builder - Custom field expressions ## Acceptance criteria - [x] Help icon (ⓘ) next to filter input opens documentation popover - [x] Field reference table + operator reference + 6+ examples in popover - [x] Autocomplete dropdown on field names (top-level + `payload.*`) - [x] Autocomplete dropdown on values for `type` / `route` operators - [x] Right-click on packet cell → "Filter ==" / "Filter !=" / "Filter contains" - [x] Right-click context menu hides when clicking elsewhere / Esc - [x] Saved-filters dropdown with at least 5 default examples (7 shipped) - [x] User-saved filters persist in localStorage - [x] Real-time match count next to filter input (already shipped pre-PR; preserved) - [ ] Improved error messages with token + position — partial: existing parse errors already cite position; not a regression - [x] No regression in existing filter behavior (`test-packet-filter.js`: 69/69 pass) --------- Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
e9c801b41a |
feat(live): filter incoming packets by IATA region (#1045) (#1080)
Closes #1045. ## What Adds an optional region dropdown to the **Live** page that filters incoming packets by observer IATA. When a user selects one or more regions, only packets observed by repeaters in those regions render in the feed/animation/audio. ## How - New `liveRegionFilter` container in the live header toggles row, initialised via the shared `RegionFilter` component in `dropdown` mode (matches packets/nodes/observers pages). - On page init, fetches `/api/observers` once and builds an `observer_id → IATA` map. - `packetMatchesRegion(packets, obsMap, selected)` (pure helper, OR across observations, case-insensitive) gates `renderPacketTree` next to the existing favorite + node filters. - Selection persists in localStorage via the existing `RegionFilter` machinery — no per-page key needed. - Listener cleanup hooked into the existing live-page teardown. ## TDD - Red commit `55097ce`: `test-live-region-filter.js` asserts `_livePacketMatchesRegion` exists and behaves correctly across 9 cases (no-selection passthrough, single match, no-match, OR across observations, multi-region selection, unknown observer, missing observer_id, case-insensitivity, observer-map override). Fails with `_livePacketMatchesRegion must be exposed` against master. - Green commit `fdec7bf`: implements helper + UI wiring + CSS; test passes. Test wired into `.github/workflows/deploy.yml` JS unit-test step. ## Notes - Server-side WS broadcast is unchanged — filtering is purely client-side, as the issue requests ("something a user can activate themselves, and not something that would be server wide"). - Pre-existing `test-live.js` / `test-live-dedup.js` failures on master are not introduced or affected by this PR (verified by running both on master HEAD). --------- Co-authored-by: meshcore-bot <bot@openclaw.local> |
||
|
|
78dabd5bda |
feat(filter): timestamp predicates (after/before/between/age) — #289 (#1070)
Fixes #289. Adds Wireshark-style timestamp predicates to the client-side packet filter engine (`public/packet-filter.js`). ## New syntax | Form | Meaning | | --- | --- | | `time after "2024-01-01"` | packets with timestamp strictly after the given datetime | | `time before "2024-12-31T23:59:59Z"` | packets strictly before | | `time between "2024-01-01" "2024-02-01"` | inclusive range (order-insensitive) | | `age < 1h` | packets newer than 1 hour | | `age > 24h` | packets older than 24 hours | | `age < 7d && type == ADVERT` | composes with existing predicates | Duration units: `s` / `m` / `h` / `d` / `w`. Datetime values use `Date.parse` (ISO 8601 + bare `YYYY-MM-DD`). `time` is also accepted as `timestamp`. ## Implementation - `OP_WORDS` extended with `after`, `before`, `between`. - New `TK.DURATION` token: lexer recognises `<number><unit>` and pre-converts to seconds at lex time (no per-evaluation parsing cost). - `between` is a two-value op handled in `parseComparison`. - Field resolver: - `time` / `timestamp` → epoch-ms; falls back to `first_seen` then `latest` so grouped rows from `/api/packets?groupByHash=true` work. - `age` → seconds since `Date.now()`. - Parse-time validation rejects invalid datetimes and unknown duration units (silent-fail would have been a footgun — every packet would just disappear). - Null/missing timestamps → predicate returns `false`, consistent with the existing null-field behaviour for `snr` / `rssi`. ## Open questions from the issue - **UTC vs local**: defaults to whatever `Date.parse` returns. Bare dates like `"2024-01-01"` are interpreted as UTC midnight by the spec. Tying this to the #286 timestamp display setting can be a follow-up. - **URL query string**: out of scope for this PR. ## Tests - New `test-packet-filter-time.js`: 20 tests covering `after`/`before`/`between`, ISO datetimes, all duration units, composition with `&&`, null-timestamp safety, invalid-datetime / invalid-unit errors, and `first_seen` fallback. - Wired into `.github/workflows/deploy.yml` JS unit-test step. - Existing `test-packet-filter.js` (69 tests) and inline self-tests still pass. ## Commits - Red: `5ccfad3` — failing tests + lexer-only stub (compiles, asserts fail) - Green: `976d50f` — implementation --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> |
||
|
|
3aaa21bbc0 |
fix(channel-decrypt): pure-JS SHA-256/HMAC fallback for HTTP context (P0 follow-up to #1021) (#1027)
## P0: PSK channel decryption silently failed on HTTP origins User reported PSK key `372a9c93260507adcbf36a84bec0f33d` "still doesn't work" after PRs #1021 (AES-ECB pure-JS) and #1024 (PSK UX) merged. Reproduced end-to-end and found the actual remaining bug. ### Root cause PR #1021 fixed the AES-ECB path by vendoring a pure-JS core, but **SHA-256 and HMAC-SHA256 in `public/channel-decrypt.js` are still pinned to `crypto.subtle`**. `SubtleCrypto` is exposed **only in secure contexts** (HTTPS / localhost); when CoreScope is served over plain HTTP — common for self-hosted instances — `crypto.subtle` is `undefined`, and: - `computeChannelHash(key)` → `Cannot read properties of undefined (reading 'digest')` - `verifyMAC(...)` → `Cannot read properties of undefined (reading 'importKey')` Both throws are swallowed by `addUserChannel`'s `try/catch`, so the only user-visible signal is the toast `"Failed to decrypt"` with no console-friendly explanation. Verdict: PR #1021 only fixed half of the crypto-in-insecure-context problem. ### Reproduction (no browser required) `test-channel-decrypt-insecure-context.js` loads the production `public/channel-decrypt.js` in a `vm` sandbox where `crypto.subtle` is undefined (mirrors HTTP browser). Pre-fix it failed 8/8 with the exact error above; post-fix it passes 8/8. ### Fix - New `public/vendor/sha256-hmac.js`: minimal pure-JS SHA-256 + HMAC-SHA256 (FIPS-180-4 + RFC 2104, ~120 LOC, MIT). Verified against Node `crypto` for SHA-256 (empty / "abc" / 1000 bytes) and RFC 4231 HMAC-SHA256 TC1. - `public/channel-decrypt.js`: `hasSubtle()` guard. `deriveKey`, `computeChannelHash`, and `verifyMAC` use `crypto.subtle` when available and fall back to `window.PureCrypto` otherwise. Same API, same return types, same async signatures. - `public/index.html`: load `vendor/sha256-hmac.js` immediately before `channel-decrypt.js` (mirrors the `vendor/aes-ecb.js` wiring from #1021). ### TDD - **Red** (`8075b55`): `test-channel-decrypt-insecure-context.js` — runs the **unmodified** prod module in a no-`subtle` sandbox, asserts on the known PSK key (hash byte `0xb7`) and synthetic encrypted packet round-trip. Compiles, runs, **fails 8/8 on assertions** (not on import errors). - **Green** (`232add6`): vendor + delegate. Test passes 8/8. - Wired into `test-all.sh` and `.github/workflows/deploy.yml` so CI gates the regression. ### Validation (all green post-fix) | Test | Result | |---|---| | `test-channel-decrypt-insecure-context.js` | 8/8 | | `test-channel-decrypt-ecb.js` (#1021 KAT) | 7/7 | | `test-channel-decrypt-m345.js` (existing) | 24/24 | | `test-channel-psk-ux.js` (#1024) | 19/19 | | `test-packet-filter.js` | 69/69 | ### Files changed - `public/vendor/sha256-hmac.js` — **new** (~150 LOC, MIT, decrypt-side only) - `public/channel-decrypt.js` — `hasSubtle()` guard + fallback in `deriveKey`/`computeChannelHash`/`verifyMAC` - `public/index.html` — script tag for `vendor/sha256-hmac.js` - `test-channel-decrypt-insecure-context.js` — **new** (8 assertions, pure Node, no browser) - `test-all.sh` + `.github/workflows/deploy.yml` — wire the test ### Risk / scope - Frontend-only, decrypt-side only. No server, schema, or config changes (Config Documentation Rule N/A). - Secure-context behaviour unchanged (still uses Web Crypto when present). - HMAC `secret` building, MAC truncation (2 bytes), and AES-ECB delegation untouched. - Hash vector for the user's PSK key matches: `SHA-256(372a9c93260507adcbf36a84bec0f33d) = b7ce04…`, channel hash byte `0xb7` (183) — confirmed against Node `crypto` and against the new pure-JS path. ### Note on the FIPS test data in the new test The PSK `372a9c93260507adcbf36a84bec0f33d` is shared test data from the bug report, not a real channel secret. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
f229e15869 |
feat(packet-filter): transport boolean + T_FLOOD/T_DIRECT route aliases (#339) (#1014)
## Summary Adds Wireshark-style filter support for transport route type to the packets-page filter engine, per #339. ## New filter syntax | Filter | Matches | |---|---| | `transport == true` | route_type 0 (TRANSPORT_FLOOD) or 3 (TRANSPORT_DIRECT) | | `transport == false` | route_type 1 (FLOOD) or 2 (DIRECT) | | `transport` | bare truthy — same as `transport == true` | | `route == T_FLOOD` | alias for `route == TRANSPORT_FLOOD` | | `route == T_DIRECT` | alias for `route == TRANSPORT_DIRECT` | | `route == TRANSPORT_FLOOD` / `TRANSPORT_DIRECT` | already worked — canonical names | Aliases are case-insensitive (`route == t_flood` works). ## Implementation - `public/packet-filter.js`: new `transport` virtual boolean field driven by `isTransportRouteType(rt)` which returns `rt === 0 || rt === 3`, mirroring `isTransportRoute()` in `cmd/server/decoder.go`. - `ROUTE_ALIASES = { t_flood: 'TRANSPORT_FLOOD', t_direct: 'TRANSPORT_DIRECT' }` resolved in the equality comparator, same pattern as the existing `TYPE_ALIASES`. - All client-side; no backend changes (issue noted this). ## Tests / TDD Red commit: `9d8fdf0` — five new assertion-failing test cases + wires `test-packet-filter.js` into CI (it existed but wasn't being executed). Green commit: `c67612b` — implementation makes all 69 tests pass. The CI wiring is part of the red commit on purpose: previously `test-packet-filter.js` was never run by CI, so a frontend filter regression couldn't fail the build. Now it can. ## CI gating proof Run `git revert c67612b` locally → `node test-packet-filter.js` reports 5 assertion failures (not build/import errors). Re-applying the green commit returns all tests to passing. Fixes #339 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
7aef3c355c |
fix(ci): freshen fixture timestamps before E2E to avoid time-based filter exclusion (#955) (#957)
## Problem The E2E fixture DB (`test-fixtures/e2e-fixture.db`) has static timestamps from March 29, 2026. The map page applies a default `lastHeard=30d` filter, so once the fixture ages past 30 days all nodes are excluded from `/api/nodes?lastHeard=30d` — causing the "Map page loads with markers" test to fail deterministically. This started blocking all CI on ~April 28, 2026 (30 days after March 29). Closes #955 (RCA #1: time-based fixture rot) ## Fix Added `tools/freshen-fixture.sh` — a small script that shifts all `last_seen`/`first_seen` timestamps forward so the newest is near `now()`, preserving relative ordering between nodes. Runs in CI before the Go server starts. Does **not** modify the checked-in fixture (no binary blob churn). ## Verification ``` $ cp test-fixtures/e2e-fixture.db /tmp/fix4.db $ bash tools/freshen-fixture.sh /tmp/fix4.db Fixture timestamps freshened in /tmp/fix4.db nodes: min=2026-05-01T07:10:00Z max=2026-05-01T14:51:33Z $ ./corescope-server -port 13585 -db /tmp/fix4.db -public public & $ curl -s "http://localhost:13585/api/nodes?limit=200&lastHeard=30d" | jq '{total, count: (.nodes | length)}' { "total": 200, "count": 200 } ``` All 200 nodes returned with the 30-day filter after freshening (vs 0 without the fix). Co-authored-by: you <you@example.com> |
||
|
|
57e272494d |
feat(server): /api/healthz readiness endpoint gated on store load (#955) (#956)
## Summary Fixes RCA #2 from #955: the HTTP listener and `/api/stats` go live before background goroutines (pickBestObservation, neighbor graph build) finish, causing CI readiness checks to pass prematurely. ## Changes 1. **`cmd/server/healthz.go`** — New `GET /api/healthz` endpoint: - Returns `503 {"ready":false,"reason":"loading"}` while background init is running - Returns `200 {"ready":true,"loadedTx":N,"loadedObs":N}` once ready 2. **`cmd/server/main.go`** — Added `sync.WaitGroup` tracking pickBestObservation and neighbor graph build goroutines. A coordinator goroutine sets `readiness.Store(1)` when all complete. `backfillResolvedPathsAsync` is NOT gated (async by design, can take 20+ min). 3. **`cmd/server/routes.go`** — Wired `/api/healthz` before system endpoints. 4. **`.github/workflows/deploy.yml`** — CI wait-for-ready loop now polls `/api/healthz` instead of `/api/stats`. 5. **`cmd/server/healthz_test.go`** — Tests for 503-before-ready, 200-after-ready, JSON shape, and anti-tautology gate. ## Rule 18 Verification Built and ran against `test-fixtures/e2e-fixture.db` (499 tx): - With the small fixture DB, init completes in <300ms so both immediate and delayed curls return 200 - Unit tests confirm 503 behavior when `readiness=0` (simulating slow init) - On production DBs with 100K+ txs, the 503 window would be 5-15s (pickBestObservation processes in 5000-tx chunks with 10ms yields) ## Test Results ``` === RUN TestHealthzNotReady --- PASS === RUN TestHealthzReady --- PASS === RUN TestHealthzAntiTautology --- PASS ok github.com/corescope/server 19.662s (full suite) ``` Co-authored-by: you <you@example.com> |
||
|
|
d81852736d |
ci: re-enable staging deploy now that VM is back (#932)
Reverts the `if: false` guard from #908. ## Why - Azure subscription was blocked, staging VM `meshcore-runner-2` deallocated. - Subscription unblocked, VM started, runner online, smoke CI [run #25117292530](https://github.com/Kpa-clawbot/CoreScope/actions/runs/25117292530) passed. - Time to resume automatic staging deploys on master pushes. ## Changes - `deploy` job: `if: false` → `if: github.event_name == 'push'` (original condition from before #908). - `publish` job: `needs: [build-and-publish]` → `needs: [deploy]` (original wiring restored). ## Verify after merge - Next master push triggers the full chain: go-test → e2e-test → build-and-publish → deploy → publish. - `docker ps` on staging VM shows `corescope-staging-go` updated to the new commit. Co-authored-by: you <you@example.com> |
||
|
|
f4484adb52 |
ci: move to GitHub-hosted runners, disable staging deploy (#908)
## Why The Azure staging VM (`meshcore-vm`) is offline. Self-hosted runners are unavailable, blocking all CI. ## What changed (per job) | Job | Change | Revert | |-----|--------|--------| | `e2e-test` | `runs-on: [self-hosted, Linux]` → `ubuntu-latest`; removed self-hosted-specific "Free disk space" step | Change `runs-on` back to `[self-hosted, Linux]`, restore disk cleanup step | | `build-and-publish` | `runs-on: [self-hosted, meshcore-runner-2]` → `ubuntu-latest`; removed "Free disk space" prune step (noop on fresh GH-hosted runners) | Change `runs-on` back, restore prune step | | `deploy` | `if: false # disabled` (was `github.event_name == 'push'`); `runs-on` kept as-is | Change `if:` back to `github.event_name == 'push'` | | `publish` | `runs-on: [self-hosted, Linux]` → `ubuntu-latest`; `needs: [deploy]` → `needs: [build-and-publish]` | Change both back | ## Notes - `go-test` and `release-artifacts` were already on `ubuntu-latest` — untouched. - The `deploy` job is disabled via `if: false` for trivial one-line revert when the VM returns. - No new `setup-*` actions were needed — `setup-node`, `setup-go`, `docker/setup-buildx-action`, and `docker/login-action` were already present. Co-authored-by: you <you@example.com> |
||
|
|
99029e41aa |
ci(#768): publish multi-arch (amd64+arm64) Docker image (#869)
## Problem
`docker pull` on ARM devices fails because the published image is
amd64-only.
## Fix
Enable multi-arch Docker builds via `docker buildx`. **Builder stage
uses native Go cross-compilation; only the runtime-stage `RUN` steps use
QEMU emulation.**
### Changes
| File | Change |
|------|--------|
| `Dockerfile` | Pin builder stage to `--platform=$BUILDPLATFORM`
(always native), accept `ARG TARGETOS`/`ARG TARGETARCH` from buildx, set
`GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0` on every `go build` |
| `.github/workflows/deploy.yml` | Add `docker/setup-buildx-action@v3` +
`docker/setup-qemu-action@v3` (latter needed only for runtime-stage
RUNs), set `platforms: linux/amd64,linux/arm64` |
### Build architecture
- **Builder stage** (`FROM --platform=$BUILDPLATFORM
golang:1.22-alpine`) — runs natively on amd64. Go toolchain
cross-compiles the binaries to `$TARGETARCH` via `GOOS/GOARCH`. No
emulation, ~10× faster than emulated builds. Works because
`modernc.org/sqlite` is pure Go (no CGO).
- **Runtime stage** (`FROM alpine:3.20`) — buildx pulls the per-arch
base. RUN steps (`apk add`, `mkdir/chown`, `chmod`) execute inside the
target-arch image, so QEMU is required to interpret arm64 binaries on
the amd64 host. Only a handful of short shell commands run under
emulation, so the QEMU cost is small.
### Verify
After merge, on an ARM device:
```bash
docker pull ghcr.io/kpa-clawbot/corescope:edge
docker inspect ghcr.io/kpa-clawbot/corescope:edge --format '{{.Architecture}}'
# → arm64
```
> First arm64 image appears on the next push to master after this
merges.
Closes #768
---------
Co-authored-by: you <you@example.com>
Co-authored-by: Kpa-clawbot <agent@corescope.local>
|
||
|
|
ff05db7367 |
ci: fix staging smoke test port — read STAGING_GO_HTTP_PORT, not hardcoded 82 (#854)
## Problem The "Deploy Staging" job's Smoke Test always fails with `Staging /api/stats did not return engine field`. Root cause: the step hardcodes `http://localhost:82/api/stats`, but `docker-compose.staging.yml:21` publishes the container on `${STAGING_GO_HTTP_PORT:-80}:80`. Default is port 80, not 82. curl gets ECONNREFUSED, `-sf` swallows the error, `grep -q engine` sees empty input → failure. Verified on staging VM: `ss -lntp` shows only `:80` listening; `docker ps` confirms `0.0.0.0:80->80/tcp`. A `curl http://localhost:82` returns connection-refused. ## Fix Read `STAGING_GO_HTTP_PORT` (same default as compose) so the smoke test tracks the port the container was actually launched on. Failure message now includes the resolved port to make future port mismatches self-diagnosing. ## Tested Logic only — the curl + grep pattern is unchanged. If any CI env override sets `STAGING_GO_HTTP_PORT`, the smoke test now follows it. Co-authored-by: Kpa-clawbot <agent@corescope.local> |
||
|
|
fa348efe2a |
fix: force-remove staging container before deploy — handles both compose and docker-run containers
The deploy step used only 'docker compose down' which can't remove containers created via 'docker run'. Now explicitly stops+removes the named container first, then runs compose down as cleanup. Permanent fix for the recurring CI deploy failure. |
||
|
|
c233c14156 |
feat: CLI tool to decrypt and export hashtag channel messages (#724)
## Summary
Adds `corescope-decrypt` — a standalone CLI tool that decrypts and
exports MeshCore hashtag channel messages from a CoreScope SQLite
database.
### What it does
MeshCore hashtag channels use symmetric encryption with keys derived
from the channel name. The CoreScope ingestor stores **all** GRP_TXT
packets, even those it can't decrypt. This tool enables retroactive
decryption — decrypt historical messages for any channel whose name you
learn after the fact.
### Architecture
- **`internal/channel/`** — Shared crypto package extracted from
ingestor logic:
- `DeriveKey()` — `SHA-256("#name")[:16]`
- `ChannelHash()` — 1-byte packet filter (`SHA-256(key)[0]`)
- `Decrypt()` — HMAC-SHA256 MAC verify + AES-128-ECB
- `ParsePlaintext()` — timestamp + flags + "sender: message" parsing
- **`cmd/decrypt/`** — CLI binary with three output formats:
- `--format json` — Full metadata (observers, path, raw hex)
- `--format html` — Self-contained interactive viewer with search/sort
- `--format irc` (or `log`) — Plain-text IRC-style log, greppable
### Usage
```bash
# JSON export
corescope-decrypt --channel "#wardriving" --db meshcore.db
# Interactive HTML viewer
corescope-decrypt --channel wardriving --db meshcore.db --format html --output wardriving.html
# Greppable log
corescope-decrypt --channel "#wardriving" --db meshcore.db --format irc | grep "KE6QR"
# From Docker
docker exec corescope-prod /app/corescope-decrypt --channel "#wardriving" --db /app/data/meshcore.db
```
### Build & deployment
- Statically linked (`CGO_ENABLED=0`) — zero dependencies
- Added to Dockerfile (available at `/app/corescope-decrypt` in
container)
- CI: builds and tests in go-test job
- CI: attaches linux/amd64 and linux/arm64 binaries to GitHub Releases
on tags
### Testing
- `internal/channel/` — 9 tests: key derivation, encrypt/decrypt
round-trip, MAC rejection, wrong-channel rejection, plaintext parsing
- `cmd/decrypt/` — 7 tests: payload extraction, channel hash
consistency, all 3 output formats, JSON parseability, fixture DB
integration
- Verified against real fixture DB: successfully decrypts 17
`#wardriving` messages
### Limitations
- Hashtag channels only (name-derived keys). Custom PSK channels not
supported.
- No DM decryption (asymmetric, per-peer keys).
- Read-only database access.
Fixes #723
---------
Co-authored-by: you <you@example.com>
|
||
|
|
00953207fb |
ci: remove arm64 build + QEMU — amd64 only
Removes linux/arm64 from multi-platform build and drops QEMU setup. All infra (prod + staging) is x86. QEMU emulation was adding ~12min to every CI run for an unused architecture. |
||
|
|
243de9fba1 |
fix: consolidate CI pipeline — build, publish to GHCR, then deploy staging (#636)
## Consolidate CI Pipeline — Build + Publish to GHCR + Deploy Staging ### What Merges the separate `publish.yml` workflow into `deploy.yml`, creating a single CI/CD pipeline: **`go-test → e2e-test → build-and-publish → deploy → publish-badges`** ### Why - Two workflows doing overlapping builds was wasteful and error-prone - `publish.yml` had a bug: `BUILD_TIME=$(date ...)` in a `with:` block never executed (literal string) - The old build job had duplicate/conflicting `APP_VERSION` assignments ### Changes - **`build-and-publish` job** replaces old `build` job — builds locally for staging, then does multi-arch GHCR push (gated to push events only, PRs skip) - **Build metadata** computed in a dedicated step, passed via `GITHUB_OUTPUT` — no more shell expansion bugs - **`APP_VERSION`** is `v1.2.3` on tag push, `edge` on master push - **Deploy** now pulls the `edge` image from GHCR and tags for compose compatibility, with fallback to local build - **`publish.yml` deleted** — no duplicate workflow - **Top-level `permissions`** block with `packages:write` for GHCR auth - **Triggers** now include `tags: ['v*']` for release publishing ### Status - ✅ Rebased onto master - ✅ Self-reviewed (all checklist items pass) - ✅ Ready for merge Co-authored-by: you <you@example.com> |
||
|
|
af9754dbea |
ci: move staging build+deploy to meshcore-runner-2
Prod VM (meshcore-vm) is now prod-only. Staging builds and deploys on the secondary runner. |
||
|
|
ddce26ff2d | ci: pin build and deploy jobs to meshcore-vm runner | ||
|
|
9f14c74b3e |
ci: add Docker cleanup before build to prevent disk space exhaustion (#473)
## Summary Fixes #472 The Docker build job on the self-hosted runner fails with `no space left on device` because Docker build cache and Go module downloads accumulate between runs. The existing cleanup (line ~330) runs in the **deploy** step *after* the build — too late to help. ## Changes - Added a "Free disk space" step at the start of the build job, **before** "Build Go Docker image": - `docker system prune -af` — removes all unused images, containers, networks - `docker builder prune -af` — clears the build cache - `df -h /` — logs available disk space for visibility - Kept the existing post-deploy cleanup as belt-and-suspenders --------- Co-authored-by: you <you@example.com> Co-authored-by: Kpa-clawbot <kpabap+clawdbot@gmail.com> |
||
|
|
38e5f02a00 |
ci: add Docker image cleanup to prevent runner disk exhaustion (#333)
## Problem The self-hosted runner (`meshcore-runner-2`) filled its 29GB disk to 100%, blocking all CI runs: ``` Filesystem Size Used Avail Use% /dev/root 29G 29G 2.3M 100% Docker Images: 67 total, 2 active, 18.83GB reclaimable (99%) ``` Root cause: no Docker image cleanup after builds. Each CI run builds a new image but never prunes old ones. ## Fix ### 1. Docker image cleanup after deploy (`deploy` job) - Runs with `if: always()` so it executes even if deploy fails - `docker image prune -af --filter "until=24h"` — removes images older than 24h (safe: current build is minutes old) - `docker builder prune -f --keep-storage=1GB` — caps build cache - Logs before/after `docker system df` for visibility ### 2. Runner log cleanup at start of E2E job - Prunes runner diagnostic logs older than 3 days (was 53MB and growing) - Reports `df -h` for disk visibility in CI output ## Impact After manual cleanup today, disk went from 100% → 35% (19GB free). This PR prevents recurrence. ## Test plan - [x] Manual cleanup verified on runner via `az vm run-command` - [ ] Next CI run should show cleanup step output in deploy job logs Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
5aa4fbb600 | chore: normalize all files to LF line endings |