mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-17 03:04:20 +00:00
41d02ffa144b2ed5d18f80dc156f088a884a5ec3
145
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ab34d9fb65 |
fix(#1206): keep VCR bar from occluding the live packet feed (#1213)
Red commit: `bcfc74de` (CI: https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1206) Fixes #1206. ## Problem On Live Map the VCR (timeline/playback) bar overlays the bottom of the viewport. Bottom-pinned overlays — the live packet feed, the legend, any corner panel — used hard-coded `bottom: 58–88px` offsets that are smaller than the real bar height (two-row mobile layout + `env(safe-area-inset-bottom)` push it to ~80px and beyond). The last N packet-feed rows slid under the bar and became unreadable / unclickable. ## Fix Publish the bar's measured height as a CSS variable on the live page and bind every bottom-anchored overlay to it. - `public/live.js` — new `initVCRHeightTracker()` runs after init; uses `ResizeObserver` + `resize` / `visualViewport.resize` to keep `--vcr-bar-height` on `.live-page` in sync with `#vcrBar`. - `public/live.css` — `.live-feed`, `.feed-show-btn`, and the `.live-overlay[data-position="bl"|"br"]` corner slots now use `bottom: calc(var(--vcr-bar-height, 58px) + 10px)`. The feed's `max-height` is also capped against `100dvh - top - vcr - margin` so its scroll container can never extend past the bar. - Stale per-breakpoint overrides (the `@supports(env(safe-area-inset))` hard-coded `78px + safe-area` for feed/legend) are removed in favor of the single tracked variable. ## TDD - Red commit `bcfc74de` adds `test-issue-1206-vcr-overlap-e2e.js`: asserts `#liveFeed.getBoundingClientRect().bottom <= #vcrBar.top` (and same for the last row) at desktop 1280x800 and mid 720x800. Verified locally that reverting the green commit makes the feed-bottom assertions fail (feed bottom 742px > VCR top 721px) — see PR body for exact numbers from the local run. - Green commit `1ad17e7f` makes all 5 assertions pass. ## Browser verified Local Go server with `test-fixtures/e2e-fixture.db`, headless Chromium via the new E2E test — all 5 assertions green. ## E2E assertion added `test-issue-1206-vcr-overlap-e2e.js:84` (bottom-row vs VCR-top) plus container check at `:74`. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <bot@corescope.local> |
||
|
|
a1f9dca951 |
fix(live #1205): re-anchor settings toggles inside MESH LIVE panel (#1219)
Red commit: f80ce5248a81fb8907081765732ebe0814f4a21a (CI URL appears in the Checks tab once the workflow starts). Supersedes closed PR #1209 with the correct approach (toggles in MESH LIVE panel, not legend). Fixes #1205. ## Problem The Live Map settings toggle row (Heat / Ghosts / Realistic / Color by hash / Matrix / Rain / Audio / Favorites / node filter / region filter — `#liveControls`) rendered as a free-floating sibling `.live-overlay` pinned `position: fixed` at bottom-right with `bottom: calc(78px + var(--bottom-nav-height) + safe-area)`. On many viewports it visually orphaned across the middle of the map, anchored to no panel. ## Regression cause PR **#1180** (commit `127a1927` — "compact header, pin controls bottom-right, narrow toggles") extracted `.live-toggles` from inside `.live-header` (the MESH LIVE panel) into a brand-new sibling `.live-overlay.live-controls` cluster. Before #1180 the toggles lived as a direct child of `.live-header`. ## Fix Restore the pre-#1180 structural pattern: `#liveControls` is re-parented as a child of `#liveHeader`, breaking onto its own row via `flex: 0 0 100%`. No more `position: fixed` overlay, no more free-floating cluster — the toggles share the MESH LIVE panel's chrome (background, blur, border, padding). - `public/live.js`: re-parent the `#liveControls` block inside `#liveHeader`, drop the `.live-overlay` class. - `public/live.css`: - `.live-controls`: `position: static`, transparent (header supplies chrome), `flex: 0 0 100%`. - `.live-header`: `flex-wrap: wrap`, `row-gap: 6px`, `max-width: calc(100vw - 24px)`; drop the `max-height: 40px` cap. Why this beats PR #1209: that PR parked toggles inside `#liveLegend`, inverting the *data → key → controls* hierarchy and pushing the legend to 60vh on mobile. Anchoring back to the MESH LIVE panel keeps controls with the panel that already labels the live surface and inherits its corner / drag affordances. ## Tests - **Red** (`test-issue-1205-live-controls-anchor-e2e.js`): asserts `#liveHeader.contains(#liveControls)` AND not contained in `#liveLegend`, parent is not `<body>` / `.live-page` directly, and the controls rect stays within the viewport. Runs at **1440×900, 640×900, 320×800**. Fails on master. - **Updated** `test-live-layout-1178-1179-e2e.js`: - (a) `.live-header-critical` height ≤ 40px (the critical strip stays compact; header itself now wraps). - (b) `.live-controls` `position: static` AND descendant of `#liveHeader` (new contract replacing the retired "fixed/right ≤24px/bottom>0"). - Wired in `.github/workflows/deploy.yml` next to the other live-layout E2Es. ## Acceptance criteria - [x] Settings toggle row renders inside the MESH LIVE panel (`#liveHeader`) - [x] Not parked in `#liveLegend` (rejected by #1209 review) - [x] Tested at desktop + tablet + narrow phone viewport widths - [x] E2E DOM assertion: parent is the MESH LIVE panel, not body / `.live-page` / `#liveLegend` --------- Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
eba9e89a72 |
fix(#1203): path-inspector — singleflight + stale-while-revalidate (#1208)
Red commit:
|
||
|
|
3255395bd0 |
fix(#1204): MESH LIVE panel — header inherited column flex from .live-overlay (#1215)
Red commit:
|
||
|
|
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: bbb98cf81aae38bff1ef77a7c8a701813b25bb77 (CI run: pending — see Checks tab) Fixes #1062. Parent: #1052. ## Gesture system Adds touch-gesture handling on phones (≤768px): 1. **Swipe-left on a packets/nodes/observers row** → reveals row-action overlay (trace, filter, copy hash). Threshold: 24% of row width OR 80px. Sub-threshold = visual peek that snaps back. 2. **Horizontal swipe on the bottom-nav strip** → advances tabs in TAB order from `bottom-nav.js`. Packets ↔ Live ↔ Map etc. 3. **Swipe-down on a slide-over panel** → calls `window.SlideOver.close()`. ## Hard constraints met - **Pointer Events ONLY** — no `touchstart`/`touchend` mixing. `setPointerCapture` for tracking continuity. - **Axis-lock** — direction committed in first 8–12px movement. Vertical scroll is never blocked unless we explicitly committed to a horizontal swipe. `body { touch-action: pan-y }` so the browser owns vertical natively. - **Leaflet exclusion** — handlers early-bail on `e.target.closest('.leaflet-container')` so pinch/pan on the map tab are untouched. - **Singleton pattern** — module-scoped `__touchGestures1062InitCount` guard. Document-level pointer listeners registered exactly once even if the script loads multiple times (mirrors the #1180 fix class). - **prefers-reduced-motion** — animations have `transition-duration: 0s` under the media query; gestures still trigger, snaps are instant. ## E2E `test-gestures-1062-e2e.js` — Playwright with synthesized PointerEvents (page.touchscreen unreliable in headless for axis-locked custom handlers). Wired into the deploy.yml matrix. E2E assertion added: test-gestures-1062-e2e.js:120 (overlay-visible after left-swipe), :201 (tab advance), :219 (Leaflet exclusion), :247 (slide-over dismiss). --------- Co-authored-by: openclaw-bot <bot@openclaw> Co-authored-by: OpenClaw Bot <bot@openclaw.dev> Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
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: a200704d5e27e47c0b29a4745bf1a1772a8876fe (CI URL added once Actions resolves the run) Fixes #1061 ## What Bottom navigation at ≤768px with 5 tabs in spec order: Home, Packets, Live, Map, Channels. Top-nav suppressed at the same breakpoint — no duplicate nav UX. ## Files - NEW `public/bottom-nav.js` — renders 5 tabs, syncs `.active` on `hashchange`, reuses the existing in-app hash router (`<a href="#/...">`). Stable selector `[data-bottom-nav-tab="<route>"]`. Container `[data-bottom-nav]`. - NEW `public/bottom-nav.css` — styles. Tokens reused: `--nav-bg`, `--nav-text`, `--nav-text-muted`, `--nav-active-bg`, `--accent`, `--border` (all global → resolve in BOTH light and dark themes). - `public/index.html` — one `<link>` for the CSS, one `<script>` after `app.js`. The `<nav>` is appended by JS as a sibling of `<main id="app">` at DOMContentLoaded. - `test-bottom-nav-1061-e2e.js` + `.github/workflows/deploy.yml` — Playwright wiring. ## Decisions - **Breakpoint:** `@media (max-width: 768px)`. No `@container` rules exist anywhere in `style.css` today — media query is consistent. - **Top-nav suppression:** `display:none` at ≤768px. Simpler than a hamburger collapse; long-tail routes (Tools/Lab/Perf) remain reachable by URL; "More"-tab/hamburger fallback deferred per issue body. - **Active indicator:** `var(--nav-active-bg)` + 2px accent top-border. No moving pill. - **Safe-area:** `padding-bottom: env(safe-area-inset-bottom)` on nav + reciprocal `body` reservation. `viewport-fit=cover` already in place. - **Reduced motion:** `prefers-reduced-motion: reduce` disables the transition. ## TDD - Red: `a200704` — assertions fail (no bottom-nav). - Green: `53851a1` — component + styles. E2E assertion added: `test-bottom-nav-1061-e2e.js:71` (case (a) — bottom-nav visible at 360x800). --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
05876b3a59 |
fix(#1173): replace #liveDot with packet-driven brand-logo node-pulse (#1177)
Red commit: PENDING (will update) Fixes #1173. Replaces the `#liveDot` WebSocket-connected indicator with a packet-driven node-pulse animation on the brand logo's two inner circles. ## Behavior (locked per issue spec) - **Animation curve:** `ease-out` (default per open-question 1). - **Rate cap:** 15/sec (66ms gap; default per open-question 2). Excess triggers are dropped, never queued. - **Direction:** alternates A→B / B→A across messages (aesthetic, not semantic). - **Idle ≥10s:** logo at full brightness, no animation. - **Disconnected:** `.logo-disconnected` applies `filter: grayscale(0.6) opacity(0.7)`. - **`prefers-reduced-motion: reduce`:** single-step `.logo-pulse-blip` on destination only. ## Implementation - WS handler hook lives in `public/app.js` `connectWS()` (`ws.onmessage` triggers `Logo.pulse()`; `ws.onopen`/`ws.onclose` toggle `Logo.setConnected()`). - `Logo` is a small IIFE in `app.js` that exposes `window.__corescopeLogo` for E2E injection. - All animation is pure CSS; JS only toggles `.logo-pulse-active` / `.logo-pulse-blip` / `.logo-disconnected`. Colors come exclusively from `--logo-accent` / `--logo-accent-hi` tokens. - Two new classes (`.logo-node-a`, `.logo-node-b`) attached to inner circles in both `.brand-logo` and `.brand-mark-only` SVGs so the mobile mark animates too. ## `#liveDot` removal proof ``` $ grep -rn liveDot public/ (no output) ``` ## E2E - E2E assertion added: `test-logo-pulse-1173-e2e.js:54` and follows. - Wired into the Playwright matrix in `.github/workflows/deploy.yml` (mirrors PR #1168 pattern from commit `5442652`). - Test injects synthetic pings via `window.__corescopeLogo.pulse({ synthetic: true })`; matches the existing harness style (no new WS-mock pattern invented). Red→green discipline preserved: the test commit lands first and CI fails on assertion; the implementation commit follows. --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
16c48e73b3 |
fix(live): compact header + pinned controls with narrow-viewport collapse (#1178, #1179) (#1180)
Red commit: 61fcc8c19b96543f1b4bbd6fd2ce54e6265d5e38 (CI run: pending — see Checks tab on this PR) Fixes #1178 Fixes #1179 ## Summary Live page layout polish — both issues touch `public/live.css` + a small `public/live.js` slice, so they ship as one PR per AGENTS rule 34. ### #1178 — Header compactness + narrow-viewport collapse - `.live-header` total height ≤ 40px at desktop widths (smaller padding, gap, title font, and pill sizing; `max-height: 40px` as a belt-and-suspenders gate). - Body wrapped in `.live-header-body` so it can collapse cleanly. - New 32×32 toggle button `[data-live-header-toggle]`, hidden at wide viewports, visible at `≤768px`. ### #1179 — Controls pinned bottom-right + narrow-viewport collapse - New `.live-controls` cluster around the toggles list and audio controls, `position: fixed; right: 12px;` and `bottom: calc(78px + var(--bottom-nav-height, 56px) + env(safe-area-inset-bottom, 0px))`. - That bottom calc reserves space for the VCR bar **and** the bottom nav (#1061, currently in PR #1174). When the bottom-nav exposes `--bottom-nav-height` the cluster tracks it; otherwise the 56px fallback keeps it clear regardless of merge order. - `z-index: 1000` keeps it above map markers but below modals. - New 32×32 toggle button `[data-live-controls-toggle]`, hidden at wide viewports, visible at `≤768px`. ### Breakpoint + selectors - Narrow = `max-width: 768px` (matches #1061 bottom-nav activation). - Stable selectors for E2E: `[data-live-header-toggle]`, `[data-live-header-body]`, `[data-live-controls-toggle]`, `[data-live-controls-body]`. No DOM-order dependence. ### Bottom-nav coexistence The expanded narrow-viewport controls panel uses `max-height: 50vh; overflow-y: auto` on its toggles list, and the cluster's `bottom` reservation guarantees the panel's bottom edge sits above the (possibly absent) bottom-nav region. The E2E test asserts exactly this with `expandedRect.bottom + 8 < innerHeight − navH`, defaulting `navH` to 56 if `.bottom-nav` is not in the DOM yet. ### Theming All new colors via existing CSS tokens (`--surface-1`, `--text`, `--text-muted`, `--border`, `--accent`). check-css-vars passes. ### TDD - Red commit: `61fcc8c` — assertions only (no impl), wired into `.github/workflows/deploy.yml` Playwright matrix. - Green commit: `7d591be` — DOM split + CSS + collapse JS. - E2E assertion added: `test-live-layout-1178-1179-e2e.js:55` (desktop header height) through `:170` (narrow controls bottom-nav coexistence). ### Local verification ``` ./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db & CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 \ node test-live-layout-1178-1179-e2e.js # → 8/8 passed ``` --------- Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
9774403fa4 |
fix(#1058): analytics chart containers — fluid + auto-stacking (#1175)
Red commit: 0f29da3 (CI is pending — will be linked once dispatched) Fixes #1058 This PR is in **red phase**. The new E2E asserts the desired fluid + auto-stacking behavior; with `master`'s code it FAILS at ≥768px (cards don't stack). Green commit follows. E2E assertion added: `test-charts-fluid-1058-e2e.js:99`. --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> Co-authored-by: Kpa-clawbot <bot@kpa-clawbot> |
||
|
|
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: 5def4d073c61058fc9f327a3c60ece27e21cbc69 (CI run pending — see Checks tab) Fixes #1087 ## What's broken (4 bugs) 1. **"QR library not loaded"** — `channel-qr.js` checked `root.QRCode` (capital), but the vendored library exports lowercase `qrcode` (Kazuhiko Arase API). Generate & Show QR always fell into the "library not loaded" branch. 2. **QR encodes `name=psk:hex`** — the Share button (and parts of the Generate path) passed the internal `psk:<hex8>` lookup key to `ChannelQR.generate`, ignoring the user's display label stored in `LABELS_KEY`. 3. **PSK channel doesn't persist on refresh** — the persistence path was scattered, and the read-back wasn't verified. Added channels disappeared on refresh and "reappeared" only when a later add ran the persist hook. 4. **Share button reuses the Add Channel modal** — wrong intent reuse (Add = INPUT, Share = OUTPUT). Replaced with a dedicated `#chShareModal` (separate DOM id, separate title, share-only affordances, privacy warning). ## TDD Red commit (this) lands ONLY the failing tests: - `test-channel-issue-1087.js` — source-string contract assertions for all 4 bugs - `test-channel-issue-1087-e2e.js` — Playwright E2E covering generate → QR render, QR display name, persistence across refresh, Share opens dedicated modal Green commit (follow-up) lands the production fixes. ## E2E assertion added E2E assertion added: test-channel-issue-1087-e2e.js:55 ## CI wiring - `test-channel-issue-1087.js` added to `.github/workflows/deploy.yml` (go-test JS unit step) + `test-all.sh` - `test-channel-issue-1087-e2e.js` added to `.github/workflows/deploy.yml` (e2e-test step) --------- Co-authored-by: bot <bot@corescope> Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
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> |
||
|
|
f59b4629b0 |
feat: publish Docker images to GHCR + simplified deploy (#610) (#627)
## Summary Implements M1-M2 of the deployment simplification spec (#610). Adds pre-built multi-arch Docker images published to GHCR, plus a simplified deploy experience for operators. **Spec:** [docs/specs/deployment-simplification.md](https://github.com/Kpa-clawbot/CoreScope/blob/master/docs/specs/deployment-simplification.md) ## Files Added (no existing files modified) ### 1. `.github/workflows/publish.yml` Multi-arch Docker publish workflow: - Triggers on `v*` tags (releases) → produces `vX.Y.Z`, `vX.Y`, `vX`, `latest` - Triggers on master push → produces `edge` (unstable) - `workflow_dispatch` for manual runs - QEMU + buildx for `linux/amd64` + `linux/arm64` - GHCR auth via `GITHUB_TOKEN` - GHA layer caching for fast rebuilds ### 2. `docker-compose.example.yml` 20-line compose file that pulls from GHCR (no local build required): - Env var overrides: `HTTP_PORT`, `DATA_DIR`, `DISABLE_CADDY`, `DISABLE_MOSQUITTO` - Health check included - Volume mount for data persistence ### 3. `DEPLOY.md` Operator documentation: - One-line `docker run` deploy - Tag reference (pinned vs latest vs edge) - Environment variables table - Update path (`docker compose pull && docker compose up -d`) - TLS options (Caddy auto-TLS vs reverse proxy) - **Migration guide for existing manage.sh users** — both paths documented with command equivalency table ## Review Status - ✅ Self-review: Actions syntax, GHCR auth, multi-arch, tag strategy, security — all verified - ✅ Torvalds: Deploy UX is clean, one-liner works, right level of simplicity - ✅ BUILD_TIME fixed: uses `date` command instead of fragile `head_commit.timestamp` - ✅ Migration guide added for existing manage.sh admins - ⚠️ `DISABLE_CADDY` env var documented but not implemented in entrypoint — pre-existing bug, filed as #629 Fixes #610 --------- Co-authored-by: you <you@example.com> |