Compare commits

..

6 Commits

Author SHA1 Message Date
you
d8f2ef199b merge master + fix flaky E2E packet detail tests
- Merge latest master (concurrent E2E + coverage, Playwright install split)
- Fix 'Packets clicking row shows detail pane' and 'Packet detail pane
  closes on ✕ click' E2E tests: wait for networkidle before interacting
  with table rows, and use waitForResponse to ensure the /packets/{hash}
  API call completes before asserting panel state. The row click triggers
  an async fetch that was silently failing in CI due to race conditions.
2026-03-29 16:17:41 +00:00
Kpa-clawbot
96ed40a3e0 fix: update engine tests to match engine-badge HTML format
Tests expected [go]/[node] text but formatVersionBadge now renders
<span class="engine-badge">go</span>. Updated 6 assertions to
check for engine-badge class and engine name in HTML output.
2026-03-29 16:02:25 +00:00
Kpa-clawbot
fb03fa80cf merge master + fix CI: add shell:bash for self-hosted runner
Resolves merge conflict with master (action version bumps).
Adds defaults.run.shell=bash to node-test job to fix PowerShell
parse errors on self-hosted runner that defaults to pwsh.
2026-03-29 15:38:02 +00:00
you
113f68aea7 ci: skip heavy CI jobs for docs-only PRs
Instead of using paths-ignore (which skips the entire workflow and
blocks required status checks), detect docs-only changes at the start
of each job and skip heavy steps while still reporting success.

This allows doc-only PRs to merge without waiting for Go builds,
Node.js tests, or Playwright E2E runs.

Reverts the approach from 7546ece (removing paths-ignore entirely)
in favor of a proper conditional skip within the jobs themselves.
2026-03-29 07:48:15 -07:00
Kpa-clawbot
f4cccdff2f ci: remove paths-ignore from pull_request trigger
PR #233 only touches .md files, which were excluded by paths-ignore,
causing CI to be skipped entirely. Remove paths-ignore from the
pull_request trigger so all PRs get validated. Keep paths-ignore on
push to avoid unnecessary deploys for docs-only changes to master.
2026-03-29 07:48:15 -07:00
Kpa-clawbot
5536b4c67c docs: remove letsmesh.net reference from README
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-29 07:48:15 -07:00
11 changed files with 98 additions and 128 deletions

View File

@@ -184,20 +184,6 @@ jobs:
with:
node-version: '22'
- name: Set up Go 1.22
if: steps.docs-check.outputs.docs_only != 'true'
uses: actions/setup-go@v6
with:
go-version: '1.22'
cache-dependency-path: cmd/server/go.sum
- name: Build Go server for E2E tests
if: steps.docs-check.outputs.docs_only != 'true'
run: |
cd cmd/server
go build -o ../../corescope-server .
echo "Go server built successfully"
- name: Install npm dependencies
if: steps.docs-check.outputs.docs_only != 'true'
run: npm ci --production=false
@@ -258,7 +244,7 @@ jobs:
# Kill any stale server on 13581
fuser -k 13581/tcp 2>/dev/null || true
sleep 2
./corescope-server -port 13581 -public public-instrumented &
COVERAGE=1 PORT=13581 node server.js &
echo $! > .server.pid
echo "Server PID: $(cat .server.pid)"
# Health-check poll loop (up to 30s)
@@ -270,7 +256,7 @@ jobs:
if [ "$i" -eq 30 ]; then
echo "Server failed to start within 30s"
echo "Last few lines from server logs:"
ps aux | grep "corescope-server" || echo "No server process found"
ps aux | grep "PORT=13581" || echo "No server process found"
exit 1
fi
sleep 1
@@ -326,7 +312,7 @@ jobs:
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'false'
run: |
fuser -k 13581/tcp 2>/dev/null || true
./corescope-server -port 13581 -public public &
PORT=13581 node server.js &
SERVER_PID=$!
# Wait for server to be ready (up to 15s)
for i in $(seq 1 15); do

View File

@@ -60,10 +60,10 @@ func (c *Config) NodeDaysOrDefault() int {
}
type HealthThresholds struct {
InfraDegradedHours float64 `json:"infraDegradedHours"`
InfraSilentHours float64 `json:"infraSilentHours"`
NodeDegradedHours float64 `json:"nodeDegradedHours"`
NodeSilentHours float64 `json:"nodeSilentHours"`
InfraDegradedMs int `json:"infraDegradedMs"`
InfraSilentMs int `json:"infraSilentMs"`
NodeDegradedMs int `json:"nodeDegradedMs"`
NodeSilentMs int `json:"nodeSilentMs"`
}
// ThemeFile mirrors theme.json overlay.
@@ -126,46 +126,34 @@ func LoadTheme(baseDirs ...string) *ThemeFile {
func (c *Config) GetHealthThresholds() HealthThresholds {
h := HealthThresholds{
InfraDegradedHours: 24,
InfraSilentHours: 72,
NodeDegradedHours: 1,
NodeSilentHours: 24,
InfraDegradedMs: 86400000,
InfraSilentMs: 259200000,
NodeDegradedMs: 3600000,
NodeSilentMs: 86400000,
}
if c.HealthThresholds != nil {
if c.HealthThresholds.InfraDegradedHours > 0 {
h.InfraDegradedHours = c.HealthThresholds.InfraDegradedHours
if c.HealthThresholds.InfraDegradedMs > 0 {
h.InfraDegradedMs = c.HealthThresholds.InfraDegradedMs
}
if c.HealthThresholds.InfraSilentHours > 0 {
h.InfraSilentHours = c.HealthThresholds.InfraSilentHours
if c.HealthThresholds.InfraSilentMs > 0 {
h.InfraSilentMs = c.HealthThresholds.InfraSilentMs
}
if c.HealthThresholds.NodeDegradedHours > 0 {
h.NodeDegradedHours = c.HealthThresholds.NodeDegradedHours
if c.HealthThresholds.NodeDegradedMs > 0 {
h.NodeDegradedMs = c.HealthThresholds.NodeDegradedMs
}
if c.HealthThresholds.NodeSilentHours > 0 {
h.NodeSilentHours = c.HealthThresholds.NodeSilentHours
if c.HealthThresholds.NodeSilentMs > 0 {
h.NodeSilentMs = c.HealthThresholds.NodeSilentMs
}
}
return h
}
// GetHealthMs returns degraded/silent thresholds in ms for a given role.
// GetHealthMs returns degraded/silent thresholds for a given role.
func (h HealthThresholds) GetHealthMs(role string) (degradedMs, silentMs int) {
const hourMs = 3600000
if role == "repeater" || role == "room" {
return int(h.InfraDegradedHours * hourMs), int(h.InfraSilentHours * hourMs)
}
return int(h.NodeDegradedHours * hourMs), int(h.NodeSilentHours * hourMs)
}
// ToClientMs returns the thresholds as ms for the frontend.
func (h HealthThresholds) ToClientMs() map[string]int {
const hourMs = 3600000
return map[string]int{
"infraDegradedMs": int(h.InfraDegradedHours * hourMs),
"infraSilentMs": int(h.InfraSilentHours * hourMs),
"nodeDegradedMs": int(h.NodeDegradedHours * hourMs),
"nodeSilentMs": int(h.NodeSilentHours * hourMs),
return h.InfraDegradedMs, h.InfraSilentMs
}
return h.NodeDegradedMs, h.NodeSilentMs
}
func (c *Config) ResolveDBPath(baseDir string) string {

View File

@@ -23,10 +23,10 @@ func TestLoadConfigValidJSON(t *testing.T) {
"SJC": "San Jose",
},
"healthThresholds": map[string]interface{}{
"infraDegradedHours": 2,
"infraSilentHours": 4,
"nodeDegradedHours": 0.5,
"nodeSilentHours": 2,
"infraDegradedMs": 100000,
"infraSilentMs": 200000,
"nodeDegradedMs": 50000,
"nodeSilentMs": 100000,
},
"liveMap": map[string]interface{}{
"propagationBufferMs": 3000,
@@ -178,68 +178,68 @@ func TestGetHealthThresholdsDefaults(t *testing.T) {
cfg := &Config{}
ht := cfg.GetHealthThresholds()
if ht.InfraDegradedHours != 24 {
t.Errorf("expected 24, got %v", ht.InfraDegradedHours)
if ht.InfraDegradedMs != 86400000 {
t.Errorf("expected 86400000, got %d", ht.InfraDegradedMs)
}
if ht.InfraSilentHours != 72 {
t.Errorf("expected 72, got %v", ht.InfraSilentHours)
if ht.InfraSilentMs != 259200000 {
t.Errorf("expected 259200000, got %d", ht.InfraSilentMs)
}
if ht.NodeDegradedHours != 1 {
t.Errorf("expected 1, got %v", ht.NodeDegradedHours)
if ht.NodeDegradedMs != 3600000 {
t.Errorf("expected 3600000, got %d", ht.NodeDegradedMs)
}
if ht.NodeSilentHours != 24 {
t.Errorf("expected 24, got %v", ht.NodeSilentHours)
if ht.NodeSilentMs != 86400000 {
t.Errorf("expected 86400000, got %d", ht.NodeSilentMs)
}
}
func TestGetHealthThresholdsCustom(t *testing.T) {
cfg := &Config{
HealthThresholds: &HealthThresholds{
InfraDegradedHours: 2,
InfraSilentHours: 4,
NodeDegradedHours: 0.5,
NodeSilentHours: 2,
InfraDegradedMs: 100000,
InfraSilentMs: 200000,
NodeDegradedMs: 50000,
NodeSilentMs: 100000,
},
}
ht := cfg.GetHealthThresholds()
if ht.InfraDegradedHours != 2 {
t.Errorf("expected 2, got %v", ht.InfraDegradedHours)
if ht.InfraDegradedMs != 100000 {
t.Errorf("expected 100000, got %d", ht.InfraDegradedMs)
}
if ht.InfraSilentHours != 4 {
t.Errorf("expected 4, got %v", ht.InfraSilentHours)
if ht.InfraSilentMs != 200000 {
t.Errorf("expected 200000, got %d", ht.InfraSilentMs)
}
if ht.NodeDegradedHours != 0.5 {
t.Errorf("expected 0.5, got %v", ht.NodeDegradedHours)
if ht.NodeDegradedMs != 50000 {
t.Errorf("expected 50000, got %d", ht.NodeDegradedMs)
}
if ht.NodeSilentHours != 2 {
t.Errorf("expected 2, got %v", ht.NodeSilentHours)
if ht.NodeSilentMs != 100000 {
t.Errorf("expected 100000, got %d", ht.NodeSilentMs)
}
}
func TestGetHealthThresholdsPartialCustom(t *testing.T) {
cfg := &Config{
HealthThresholds: &HealthThresholds{
InfraDegradedHours: 2,
InfraDegradedMs: 100000,
// Others left as zero → should use defaults
},
}
ht := cfg.GetHealthThresholds()
if ht.InfraDegradedHours != 2 {
t.Errorf("expected 2, got %v", ht.InfraDegradedHours)
if ht.InfraDegradedMs != 100000 {
t.Errorf("expected 100000, got %d", ht.InfraDegradedMs)
}
if ht.InfraSilentHours != 72 {
t.Errorf("expected default 72, got %v", ht.InfraSilentHours)
if ht.InfraSilentMs != 259200000 {
t.Errorf("expected default 259200000, got %d", ht.InfraSilentMs)
}
}
func TestGetHealthMs(t *testing.T) {
ht := HealthThresholds{
InfraDegradedHours: 24,
InfraSilentHours: 72,
NodeDegradedHours: 1,
NodeSilentHours: 24,
InfraDegradedMs: 86400000,
InfraSilentMs: 259200000,
NodeDegradedMs: 3600000,
NodeSilentMs: 86400000,
}
tests := []struct {

View File

@@ -513,10 +513,10 @@ func TestGetNetworkStatus(t *testing.T) {
seedTestData(t, db)
ht := HealthThresholds{
InfraDegradedHours: 24,
InfraSilentHours: 72,
NodeDegradedHours: 1,
NodeSilentHours: 24,
InfraDegradedMs: 86400000,
InfraSilentMs: 259200000,
NodeDegradedMs: 3600000,
NodeSilentMs: 86400000,
}
result, err := db.GetNetworkStatus(ht)
if err != nil {
@@ -1050,10 +1050,10 @@ func TestGetNetworkStatusDateFormats(t *testing.T) {
VALUES ('node4444', 'NodeBad', 'sensor', 'not-a-date')`)
ht := HealthThresholds{
InfraDegradedHours: 24,
InfraSilentHours: 72,
NodeDegradedHours: 1,
NodeSilentHours: 24,
InfraDegradedMs: 86400000,
InfraSilentMs: 259200000,
NodeDegradedMs: 3600000,
NodeSilentMs: 86400000,
}
result, err := db.GetNetworkStatus(ht)
if err != nil {

View File

@@ -213,7 +213,7 @@ func (s *Server) handleConfigCache(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleConfigClient(w http.ResponseWriter, r *http.Request) {
writeJSON(w, ClientConfigResponse{
Roles: s.cfg.Roles,
HealthThresholds: s.cfg.GetHealthThresholds().ToClientMs(),
HealthThresholds: s.cfg.HealthThresholds,
Tiles: s.cfg.Tiles,
SnrThresholds: s.cfg.SnrThresholds,
DistThresholds: s.cfg.DistThresholds,

View File

@@ -98,13 +98,6 @@
"#bookclub",
"#shtf"
],
"healthThresholds": {
"infraDegradedHours": 24,
"infraSilentHours": 72,
"nodeDegradedHours": 1,
"nodeSilentHours": 24,
"_comment": "How long (hours) before nodes show as degraded/silent. 'infra' = repeaters & rooms, 'node' = companions & others."
},
"defaultRegion": "SJC",
"mapDefaults": {
"center": [

View File

@@ -89,8 +89,7 @@
function getStatusTooltip(role, status) {
const isInfra = role === 'repeater' || role === 'room';
const threshMs = isInfra ? HEALTH_THRESHOLDS.infraSilentMs : HEALTH_THRESHOLDS.nodeSilentMs;
const threshold = threshMs >= 3600000 ? Math.round(threshMs / 3600000) + 'h' : Math.round(threshMs / 60000) + 'm';
const threshold = isInfra ? '72h' : '24h';
if (status === 'active') {
return 'Active \u2014 heard within the last ' + threshold + '.' + (isInfra ? ' Repeaters typically advertise every 12-24h.' : '');
}

View File

@@ -36,19 +36,18 @@ function loadThemeFile(themePaths) {
function buildHealthConfig(config) {
const _ht = (config && config.healthThresholds) || {};
return {
infraDegraded: _ht.infraDegradedHours || 24,
infraSilent: _ht.infraSilentHours || 72,
nodeDegraded: _ht.nodeDegradedHours || 1,
nodeSilent: _ht.nodeSilentHours || 24
infraDegradedMs: _ht.infraDegradedMs || 86400000,
infraSilentMs: _ht.infraSilentMs || 259200000,
nodeDegradedMs: _ht.nodeDegradedMs || 3600000,
nodeSilentMs: _ht.nodeSilentMs || 86400000
};
}
function getHealthMs(role, HEALTH) {
const H = 3600000;
const isInfra = role === 'repeater' || role === 'room';
return {
degradedMs: (isInfra ? HEALTH.infraDegraded : HEALTH.nodeDegraded) * H,
silentMs: (isInfra ? HEALTH.infraSilent : HEALTH.nodeSilent) * H
degradedMs: isInfra ? HEALTH.infraDegradedMs : HEALTH.nodeDegradedMs,
silentMs: isInfra ? HEALTH.infraSilentMs : HEALTH.nodeSilentMs
};
}

View File

@@ -307,12 +307,7 @@ app.get('/api/config/cache', (req, res) => {
app.get('/api/config/client', (req, res) => {
res.json({
roles: config.roles || null,
healthThresholds: {
infraDegradedMs: HEALTH.infraDegraded * 3600000,
infraSilentMs: HEALTH.infraSilent * 3600000,
nodeDegradedMs: HEALTH.nodeDegraded * 3600000,
nodeSilentMs: HEALTH.nodeSilent * 3600000
},
healthThresholds: config.healthThresholds || null,
tiles: config.tiles || null,
snrThresholds: config.snrThresholds || null,
distThresholds: config.distThresholds || null,

View File

@@ -840,7 +840,17 @@ async function run() {
assert(content.length > 10, 'Perf content should still be present after refresh');
});
// Test: Node.js perf page shows Event Loop metrics (not Go Runtime)
await test('Perf page shows Event Loop on Node server', async () => {
const perfText = await page.$eval('#perfContent', el => el.textContent);
// Node.js server should show Event Loop metrics
const hasEventLoop = perfText.includes('Event Loop') || perfText.includes('event loop');
const hasMemory = perfText.includes('Memory') || perfText.includes('RSS');
assert(hasEventLoop || hasMemory, 'Node perf page should show Event Loop or Memory metrics');
// Should NOT show Go Runtime section on Node.js server
const hasGoRuntime = perfText.includes('Go Runtime');
assert(!hasGoRuntime, 'Node perf page should NOT show Go Runtime section');
});
// Test: Go perf page shows Go Runtime section (goroutines, GC)
// NOTE: This test requires GO_BASE_URL pointing to Go staging (port 82)

View File

@@ -59,17 +59,17 @@ console.log('\nloadThemeFile:');
console.log('\nbuildHealthConfig:');
{
const h = helpers.buildHealthConfig({});
assert(h.infraDegraded === 24, 'default infraDegraded');
assert(h.infraSilent === 72, 'default infraSilent');
assert(h.nodeDegraded === 1, 'default nodeDegraded');
assert(h.nodeSilent === 24, 'default nodeSilent');
assert(h.infraDegradedMs === 86400000, 'default infraDegradedMs');
assert(h.infraSilentMs === 259200000, 'default infraSilentMs');
assert(h.nodeDegradedMs === 3600000, 'default nodeDegradedMs');
assert(h.nodeSilentMs === 86400000, 'default nodeSilentMs');
const h2 = helpers.buildHealthConfig({ healthThresholds: { infraDegradedHours: 2 } });
assert(h2.infraDegraded === 2, 'custom infraDegraded');
assert(h2.nodeDegraded === 1, 'other defaults preserved');
const h2 = helpers.buildHealthConfig({ healthThresholds: { infraDegradedMs: 1000 } });
assert(h2.infraDegradedMs === 1000, 'custom infraDegradedMs');
assert(h2.nodeDegradedMs === 3600000, 'other defaults preserved');
const h3 = helpers.buildHealthConfig(null);
assert(h3.infraDegraded === 24, 'handles null config');
assert(h3.infraDegradedMs === 86400000, 'handles null config');
}
// --- getHealthMs ---
@@ -78,21 +78,21 @@ console.log('\ngetHealthMs:');
const HEALTH = helpers.buildHealthConfig({});
const rep = helpers.getHealthMs('repeater', HEALTH);
assert(rep.degradedMs === 24 * 3600000, 'repeater uses infra degraded');
assert(rep.silentMs === 72 * 3600000, 'repeater uses infra silent');
assert(rep.degradedMs === 86400000, 'repeater uses infra degraded');
assert(rep.silentMs === 259200000, 'repeater uses infra silent');
const room = helpers.getHealthMs('room', HEALTH);
assert(room.degradedMs === 24 * 3600000, 'room uses infra degraded');
assert(room.degradedMs === 86400000, 'room uses infra degraded');
const comp = helpers.getHealthMs('companion', HEALTH);
assert(comp.degradedMs === 1 * 3600000, 'companion uses node degraded');
assert(comp.silentMs === 24 * 3600000, 'companion uses node silent');
assert(comp.degradedMs === 3600000, 'companion uses node degraded');
assert(comp.silentMs === 86400000, 'companion uses node silent');
const sensor = helpers.getHealthMs('sensor', HEALTH);
assert(sensor.degradedMs === 1 * 3600000, 'sensor uses node degraded');
assert(sensor.degradedMs === 3600000, 'sensor uses node degraded');
const undef = helpers.getHealthMs(undefined, HEALTH);
assert(undef.degradedMs === 1 * 3600000, 'undefined role uses node degraded');
assert(undef.degradedMs === 3600000, 'undefined role uses node degraded');
}
// --- isHashSizeFlipFlop ---