Compare commits

..

2 Commits

Author SHA1 Message Date
Kpa-clawbot
7394f00fec ci: force bash shell for all workflow steps
Self-hosted runner defaults to PowerShell on Windows, causing bash
syntax (if/then/fi, curl line continuations) to fail with parse errors.
Setting defaults.run.shell=bash at workflow level fixes all steps.
2026-03-29 18:29:04 +00:00
efiten
464e7b3ea4 fix: stop wiping analytics cache on every ingest cycle
The 15s TTL already handles freshness — clearing all cache maps on
every 1-second poll meant entries were never reused, giving 0% server
hit rate and forcing every analytics request back to SQLite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 18:28:43 +00:00
18 changed files with 471 additions and 546 deletions

View File

@@ -0,0 +1 @@
{"schemaVersion":1,"label":"backend coverage","message":"87.79%","color":"brightgreen"}

View File

@@ -0,0 +1 @@
{"schemaVersion":1,"label":"backend tests","message":"998 passed","color":"brightgreen"}

1
.badges/coverage.json Normal file
View File

@@ -0,0 +1 @@
{"schemaVersion":1,"label":"coverage","message":"76%","color":"yellow"}

View File

@@ -1 +0,0 @@
{"schemaVersion":1,"label":"e2e tests","message":"pending","color":"lightgrey"}

View File

@@ -1 +0,0 @@
{"schemaVersion":1,"label":"go ingestor coverage","message":"pending","color":"lightgrey"}

View File

@@ -1 +0,0 @@
{"schemaVersion":1,"label":"go server coverage","message":"pending","color":"lightgrey"}

1
.badges/tests.json Normal file
View File

@@ -0,0 +1 @@
{"schemaVersion":1,"label":"tests","message":"844/844 passed","color":"brightgreen"}

View File

@@ -1,331 +1,324 @@
name: CI/CD Pipeline
on:
push:
branches: [master]
pull_request:
branches: [master]
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
STAGING_COMPOSE_FILE: docker-compose.staging.yml
STAGING_SERVICE: staging-go
STAGING_CONTAINER: corescope-staging-go
# Pipeline (sequential, fail-fast):
# go-test → e2e-test → build → deploy → publish
# PRs stop after build. Master continues to deploy + publish.
jobs:
# ───────────────────────────────────────────────────────────────
# 1. Go Build & Test
# ───────────────────────────────────────────────────────────────
go-test:
name: "✅ Go Build & Test"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Clean Go module cache
run: rm -rf ~/go/pkg/mod 2>/dev/null || true
- name: Set up Go 1.22
uses: actions/setup-go@v6
with:
go-version: '1.22'
cache-dependency-path: |
cmd/server/go.sum
cmd/ingestor/go.sum
- name: Build and test Go server (with coverage)
run: |
set -e -o pipefail
cd cmd/server
go build .
go test -coverprofile=server-coverage.out ./... 2>&1 | tee server-test.log
echo "--- Go Server Coverage ---"
go tool cover -func=server-coverage.out | tail -1
- name: Build and test Go ingestor (with coverage)
run: |
set -e -o pipefail
cd cmd/ingestor
go build .
go test -coverprofile=ingestor-coverage.out ./... 2>&1 | tee ingestor-test.log
echo "--- Go Ingestor Coverage ---"
go tool cover -func=ingestor-coverage.out | tail -1
- name: Verify proto syntax
run: |
set -e
sudo apt-get update -qq
sudo apt-get install -y protobuf-compiler
for proto in proto/*.proto; do
echo " ✓ $(basename "$proto")"
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
done
echo "✅ All .proto files are syntactically valid"
- name: Generate Go coverage badges
if: success()
run: |
mkdir -p .badges
SERVER_COV="0"
if [ -f cmd/server/server-coverage.out ]; then
SERVER_COV=$(cd cmd/server && go tool cover -func=server-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
fi
SERVER_COLOR="red"
if [ "$(echo "$SERVER_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="green"
elif [ "$(echo "$SERVER_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="yellow"; fi
echo "{\"schemaVersion\":1,\"label\":\"go server coverage\",\"message\":\"${SERVER_COV}%\",\"color\":\"${SERVER_COLOR}\"}" > .badges/go-server-coverage.json
INGESTOR_COV="0"
if [ -f cmd/ingestor/ingestor-coverage.out ]; then
INGESTOR_COV=$(cd cmd/ingestor && go tool cover -func=ingestor-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
fi
INGESTOR_COLOR="red"
if [ "$(echo "$INGESTOR_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="green"
elif [ "$(echo "$INGESTOR_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="yellow"; fi
echo "{\"schemaVersion\":1,\"label\":\"go ingestor coverage\",\"message\":\"${INGESTOR_COV}%\",\"color\":\"${INGESTOR_COLOR}\"}" > .badges/go-ingestor-coverage.json
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
echo "|--------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
- name: Upload Go coverage badges
if: success()
uses: actions/upload-artifact@v6
with:
name: go-badges
path: .badges/go-*.json
retention-days: 1
if-no-files-found: ignore
# ───────────────────────────────────────────────────────────────
# 2. Playwright E2E Tests (against Go server with fixture DB)
# ───────────────────────────────────────────────────────────────
e2e-test:
name: "🎭 Playwright E2E Tests"
needs: [go-test]
runs-on: [self-hosted, Linux]
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Node.js 22
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Clean Go module cache
run: rm -rf ~/go/pkg/mod 2>/dev/null || true
- name: Set up Go 1.22
uses: actions/setup-go@v6
with:
go-version: '1.22'
cache-dependency-path: cmd/server/go.sum
- name: Build Go server
run: |
cd cmd/server
go build -o ../../corescope-server .
echo "Go server built successfully"
- name: Install npm dependencies
run: npm ci --production=false
- name: Install Playwright browser
run: |
npx playwright install chromium 2>/dev/null || true
npx playwright install-deps chromium 2>/dev/null || true
- name: Instrument frontend JS for coverage
run: sh scripts/instrument-frontend.sh
- name: Start Go server with fixture DB
run: |
fuser -k 13581/tcp 2>/dev/null || true
sleep 1
./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public-instrumented &
echo $! > .server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:13581/api/stats > /dev/null 2>&1; then
echo "Server ready after ${i}s"
break
fi
if [ "$i" -eq 30 ]; then
echo "Server failed to start within 30s"
exit 1
fi
sleep 1
done
- name: Run Playwright E2E tests (fail-fast)
run: |
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
# DISABLED: Coverage collector takes 8+ min. E2E tests extract window.__coverage__ directly.
# - name: Collect frontend coverage
# if: success()
# run: |
# BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt || true
- name: Generate frontend coverage badges
if: success()
run: |
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1 || echo "0")
mkdir -p .badges
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
FE_COVERAGE=${FE_COVERAGE:-0}
FE_COLOR="red"
[ "$(echo "$FE_COVERAGE > 50" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="yellow"
[ "$(echo "$FE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="brightgreen"
echo "{\"schemaVersion\":1,\"label\":\"frontend coverage\",\"message\":\"${FE_COVERAGE}%\",\"color\":\"${FE_COLOR}\"}" > .badges/frontend-coverage.json
echo "## Frontend: ${FE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
fi
echo "{\"schemaVersion\":1,\"label\":\"e2e tests\",\"message\":\"${E2E_PASS:-0} passed\",\"color\":\"brightgreen\"}" > .badges/e2e-tests.json
- name: Stop test server
if: success()
run: |
if [ -f .server.pid ]; then
kill $(cat .server.pid) 2>/dev/null || true
rm -f .server.pid
fi
- name: Upload E2E badges
if: success()
uses: actions/upload-artifact@v6
with:
name: e2e-badges
path: .badges/
retention-days: 1
if-no-files-found: ignore
# ───────────────────────────────────────────────────────────────
# 3. Build Docker Image
# ───────────────────────────────────────────────────────────────
build:
name: "🏗️ Build Docker Image"
needs: [e2e-test]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Node.js 22
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Build Go Docker image
run: |
echo "${GITHUB_SHA::7}" > .git-commit
APP_VERSION=$(node -p "require('./package.json').version") \
GIT_COMMIT="${GITHUB_SHA::7}" \
APP_VERSION=$(grep -oP 'APP_VERSION:-\K[^}]+' docker-compose.yml | head -1 || echo "3.0.0")
GIT_COMMIT=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
export APP_VERSION GIT_COMMIT BUILD_TIME
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging build "$STAGING_SERVICE"
echo "Built Go staging image ✅"
# ───────────────────────────────────────────────────────────────
# 4. Deploy Staging (master only)
# ───────────────────────────────────────────────────────────────
deploy:
name: "🚀 Deploy Staging"
if: github.event_name == 'push'
needs: [build]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Deploy staging
run: ./manage.sh restart staging
- name: Healthcheck staging container
run: |
for i in $(seq 1 120); do
HEALTH=$(docker inspect corescope-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
if [ "$HEALTH" = "healthy" ]; then
echo "Staging healthy after ${i}s"
break
fi
if [ "$i" -eq 120 ]; then
echo "Staging failed health check after 120s"
docker logs corescope-staging-go --tail 50
exit 1
fi
sleep 1
done
- name: Smoke test staging API
run: |
if curl -sf http://localhost:82/api/stats | grep -q engine; then
echo "Staging verified — engine field present ✅"
else
echo "Staging /api/stats did not return engine field"
exit 1
fi
# ───────────────────────────────────────────────────────────────
# 5. Publish Badges & Summary (master only)
# ───────────────────────────────────────────────────────────────
publish:
name: "📝 Publish Badges & Summary"
if: github.event_name == 'push'
needs: [deploy]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download Go coverage badges
continue-on-error: true
uses: actions/download-artifact@v6
with:
name: go-badges
path: .badges/
- name: Download E2E badges
continue-on-error: true
uses: actions/download-artifact@v6
with:
name: e2e-badges
path: .badges/
- name: Publish coverage badges to repo
continue-on-error: true
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git remote set-url origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
git add .badges/ -f
git diff --cached --quiet || (git commit -m "ci: update test badges [skip ci]" && git push) || echo "Badge push failed"
- name: Post deployment summary
run: |
echo "## Staging Deployed ✓" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`$(git rev-parse --short HEAD)\` — $(git log -1 --format=%s)" >> $GITHUB_STEP_SUMMARY
name: CI/CD Pipeline
on:
push:
branches: [master]
pull_request:
branches: [master]
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Pipeline (sequential, fail-fast):
# go-test → e2e-test → build → deploy publish
# PRs stop after build. Master continues to deploy + publish.
jobs:
# ───────────────────────────────────────────────────────────────
# 1. Go Build & Test
# ───────────────────────────────────────────────────────────────
go-test:
name: "✅ Go Build & Test"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go 1.22
uses: actions/setup-go@v6
with:
go-version: '1.22'
cache-dependency-path: |
cmd/server/go.sum
cmd/ingestor/go.sum
- name: Build and test Go server (with coverage)
run: |
set -e -o pipefail
cd cmd/server
go build .
go test -coverprofile=server-coverage.out ./... 2>&1 | tee server-test.log
echo "--- Go Server Coverage ---"
go tool cover -func=server-coverage.out | tail -1
- name: Build and test Go ingestor (with coverage)
run: |
set -e -o pipefail
cd cmd/ingestor
go build .
go test -coverprofile=ingestor-coverage.out ./... 2>&1 | tee ingestor-test.log
echo "--- Go Ingestor Coverage ---"
go tool cover -func=ingestor-coverage.out | tail -1
- name: Verify proto syntax
run: |
set -e
sudo apt-get update -qq
sudo apt-get install -y protobuf-compiler
for proto in proto/*.proto; do
echo " ✓ $(basename "$proto")"
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
done
echo "✅ All .proto files are syntactically valid"
- name: Generate Go coverage badges
if: success()
run: |
mkdir -p .badges
SERVER_COV="0"
if [ -f cmd/server/server-coverage.out ]; then
SERVER_COV=$(cd cmd/server && go tool cover -func=server-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
fi
SERVER_COLOR="red"
if [ "$(echo "$SERVER_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="green"
elif [ "$(echo "$SERVER_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="yellow"; fi
echo "{\"schemaVersion\":1,\"label\":\"go server coverage\",\"message\":\"${SERVER_COV}%\",\"color\":\"${SERVER_COLOR}\"}" > .badges/go-server-coverage.json
INGESTOR_COV="0"
if [ -f cmd/ingestor/ingestor-coverage.out ]; then
INGESTOR_COV=$(cd cmd/ingestor && go tool cover -func=ingestor-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
fi
INGESTOR_COLOR="red"
if [ "$(echo "$INGESTOR_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="green"
elif [ "$(echo "$INGESTOR_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="yellow"; fi
echo "{\"schemaVersion\":1,\"label\":\"go ingestor coverage\",\"message\":\"${INGESTOR_COV}%\",\"color\":\"${INGESTOR_COLOR}\"}" > .badges/go-ingestor-coverage.json
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
echo "|--------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
- name: Upload Go coverage badges
if: success()
uses: actions/upload-artifact@v6
with:
name: go-badges
path: .badges/go-*.json
retention-days: 1
if-no-files-found: ignore
# ───────────────────────────────────────────────────────────────
# 2. Playwright E2E Tests (against Go server with fixture DB)
# ───────────────────────────────────────────────────────────────
e2e-test:
name: "🎭 Playwright E2E Tests"
needs: [go-test]
runs-on: [self-hosted, Linux]
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Node.js 22
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Set up Go 1.22
uses: actions/setup-go@v6
with:
go-version: '1.22'
cache-dependency-path: cmd/server/go.sum
- name: Build Go server
run: |
cd cmd/server
go build -o ../../corescope-server .
echo "Go server built successfully"
- name: Install npm dependencies
run: npm ci --production=false
- name: Install Playwright browser
run: |
npx playwright install chromium 2>/dev/null || true
npx playwright install-deps chromium 2>/dev/null || true
- name: Instrument frontend JS for coverage
run: sh scripts/instrument-frontend.sh
- name: Start Go server with fixture DB
run: |
fuser -k 13581/tcp 2>/dev/null || true
sleep 1
./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public-instrumented &
echo $! > .server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:13581/api/stats > /dev/null 2>&1; then
echo "Server ready after ${i}s"
break
fi
if [ "$i" -eq 30 ]; then
echo "Server failed to start within 30s"
exit 1
fi
sleep 1
done
- name: Run Playwright E2E tests (fail-fast)
run: |
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
- name: Collect frontend coverage
if: success()
run: |
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt || true
- name: Generate frontend coverage badges
if: success()
run: |
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1 || echo "0")
mkdir -p .badges
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
FE_COVERAGE=${FE_COVERAGE:-0}
FE_COLOR="red"
[ "$(echo "$FE_COVERAGE > 50" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="yellow"
[ "$(echo "$FE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="brightgreen"
echo "{\"schemaVersion\":1,\"label\":\"frontend coverage\",\"message\":\"${FE_COVERAGE}%\",\"color\":\"${FE_COLOR}\"}" > .badges/frontend-coverage.json
echo "## Frontend: ${FE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
fi
echo "{\"schemaVersion\":1,\"label\":\"e2e tests\",\"message\":\"${E2E_PASS:-0} passed\",\"color\":\"brightgreen\"}" > .badges/e2e-tests.json
- name: Stop test server
if: success()
run: |
if [ -f .server.pid ]; then
kill $(cat .server.pid) 2>/dev/null || true
rm -f .server.pid
fi
- name: Upload E2E badges
if: success()
uses: actions/upload-artifact@v6
with:
name: e2e-badges
path: .badges/
retention-days: 1
if-no-files-found: ignore
# ───────────────────────────────────────────────────────────────
# 3. Build Docker Image
# ───────────────────────────────────────────────────────────────
build:
name: "🏗️ Build Docker Image"
needs: [e2e-test]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Node.js 22
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Build Go Docker image
run: |
echo "${GITHUB_SHA::7}" > .git-commit
APP_VERSION=$(node -p "require('./package.json').version") \
GIT_COMMIT="${GITHUB_SHA::7}" \
docker compose --profile staging-go build staging-go
echo "Built Go staging image ✅"
# ───────────────────────────────────────────────────────────────
# 4. Deploy Staging (master only)
# ───────────────────────────────────────────────────────────────
deploy:
name: "🚀 Deploy Staging"
if: github.event_name == 'push'
needs: [build]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Start staging on port 82
run: |
docker rm -f corescope-staging-go 2>/dev/null || true
fuser -k 82/tcp 2>/dev/null || true
docker compose --profile staging-go up -d staging-go
- name: Healthcheck staging container
run: |
for i in $(seq 1 120); do
HEALTH=$(docker inspect corescope-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
if [ "$HEALTH" = "healthy" ]; then
echo "Staging healthy after ${i}s"
break
fi
if [ "$i" -eq 120 ]; then
echo "Staging failed health check after 120s"
docker logs corescope-staging-go --tail 50
exit 1
fi
sleep 1
done
- name: Smoke test staging API
run: |
if curl -sf http://localhost:82/api/stats | grep -q engine; then
echo "Staging verified — engine field present ✅"
else
echo "Staging /api/stats did not return engine field"
exit 1
fi
# ───────────────────────────────────────────────────────────────
# 5. Publish Badges & Summary (master only)
# ───────────────────────────────────────────────────────────────
publish:
name: "📝 Publish Badges & Summary"
if: github.event_name == 'push'
needs: [deploy]
runs-on: [self-hosted, Linux]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download Go coverage badges
continue-on-error: true
uses: actions/download-artifact@v6
with:
name: go-badges
path: .badges/
- name: Download E2E badges
continue-on-error: true
uses: actions/download-artifact@v6
with:
name: e2e-badges
path: .badges/
- name: Publish coverage badges to repo
continue-on-error: true
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git remote set-url origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
git add .badges/ -f
git diff --cached --quiet || (git commit -m "ci: update test badges [skip ci]" && git push) || echo "Badge push failed"
- name: Post deployment summary
run: |
echo "## Staging Deployed ✓" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`$(git rev-parse --short HEAD)\` — $(git log -1 --format=%s)" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,10 +0,0 @@
{
"include": [
"public/*.js"
],
"exclude": [
"public/vendor/**",
"public/leaflet-*.js",
"public/qrcode*.js"
]
}

View File

@@ -1,10 +1,10 @@
# CoreScope
[![Go Server Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/CoreScope/master/.badges/go-server-coverage.json)](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
[![Go Ingestor Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/CoreScope/master/.badges/go-ingestor-coverage.json)](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
[![E2E Tests](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/CoreScope/master/.badges/e2e-tests.json)](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
[![Frontend Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/CoreScope/master/.badges/frontend-coverage.json)](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
[![Deploy](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml/badge.svg)](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
[![Go Server Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/.badges/go-server-coverage.json)](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
[![Go Ingestor Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/.badges/go-ingestor-coverage.json)](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
[![Frontend Tests](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/.badges/frontend-tests.json)](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
[![Frontend Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/.badges/frontend-coverage.json)](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
[![Deploy](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml/badge.svg)](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
> High-performance mesh network analyzer powered by Go. Sub-millisecond packet queries, ~300 MB memory for 56K+ packets, real-time WebSocket broadcast, full channel decryption.
@@ -79,7 +79,7 @@ Full experience on your phone — proper touch controls, iOS safe area support,
No Go installation needed — everything builds inside the container.
```bash
git clone https://github.com/Kpa-clawbot/CoreScope.git
git clone https://github.com/Kpa-clawbot/corescope.git
cd corescope
./manage.sh setup
```

View File

@@ -1085,18 +1085,6 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac
}
}
// Invalidate analytics caches since new data was ingested
if len(result) > 0 {
s.cacheMu.Lock()
s.rfCache = make(map[string]*cachedResult)
s.topoCache = make(map[string]*cachedResult)
s.hashCache = make(map[string]*cachedResult)
s.chanCache = make(map[string]*cachedResult)
s.distCache = make(map[string]*cachedResult)
s.subpathCache = make(map[string]*cachedResult)
s.cacheMu.Unlock()
}
return result, newMaxID
}
@@ -1301,20 +1289,6 @@ func (s *PacketStore) IngestNewObservations(sinceObsID, limit int) []map[string]
}
}
if len(updatedTxs) > 0 {
// Invalidate analytics caches
s.cacheMu.Lock()
s.rfCache = make(map[string]*cachedResult)
s.topoCache = make(map[string]*cachedResult)
s.hashCache = make(map[string]*cachedResult)
s.chanCache = make(map[string]*cachedResult)
s.distCache = make(map[string]*cachedResult)
s.subpathCache = make(map[string]*cachedResult)
s.cacheMu.Unlock()
// analytics caches cleared; no per-cycle log to avoid stdout overhead
}
return broadcastMaps
}

View File

@@ -1,38 +0,0 @@
# Staging-only compose file. Production is managed by docker-compose.yml.
# Override defaults via .env or environment variables.
services:
staging-go:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
BUILD_TIME: ${BUILD_TIME:-unknown}
image: corescope-go:latest
container_name: corescope-staging-go
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${STAGING_GO_HTTP_PORT:-82}:80"
- "${STAGING_GO_MQTT_PORT:-1885}:1883"
- "6060:6060" # pprof server
- "6061:6061" # pprof ingestor
volumes:
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}/config.json:/app/config.json:ro
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}:/app/data
- caddy-data-staging-go:/data/caddy
environment:
- NODE_ENV=staging
- ENABLE_PPROF=true
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
timeout: 5s
retries: 3
volumes:
# Named volume for Caddy TLS certificates (not user data — managed by Caddy internally)
caddy-data-staging-go:

View File

@@ -1,38 +1,92 @@
# All container config lives here. manage.sh is just a wrapper around docker compose.
# Override defaults via .env or environment variables.
# CRITICAL: All data mounts use bind mounts (~/path), NOT named volumes.
# This ensures the DB and theme are visible on the host filesystem for backup.
services:
prod:
build:
context: .
args:
APP_VERSION: ${APP_VERSION:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
BUILD_TIME: ${BUILD_TIME:-unknown}
image: corescope:latest
container_name: corescope-prod
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${PROD_HTTP_PORT:-80}:${PROD_HTTP_PORT:-80}"
- "${PROD_HTTPS_PORT:-443}:${PROD_HTTPS_PORT:-443}"
- "${PROD_MQTT_PORT:-1883}:1883"
volumes:
- ./config.json:/app/config.json:ro
- ./caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro
- ${PROD_DATA_DIR:-~/meshcore-data}:/app/data
- caddy-data:/data/caddy
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
timeout: 5s
retries: 3
volumes:
# Named volumes for Caddy TLS certificates (not user data — managed by Caddy internally)
# All container config lives here. manage.sh is just a wrapper around docker compose.
# Override defaults via .env or environment variables.
# CRITICAL: All data mounts use bind mounts (~/path), NOT named volumes.
# This ensures the DB and theme are visible on the host filesystem for backup.
services:
prod:
build: .
image: corescope:latest
container_name: corescope-prod
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${PROD_HTTP_PORT:-80}:${PROD_HTTP_PORT:-80}"
- "${PROD_HTTPS_PORT:-443}:${PROD_HTTPS_PORT:-443}"
- "${PROD_MQTT_PORT:-1883}:1883"
volumes:
- ./config.json:/app/config.json:ro
- ./caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro
- ${PROD_DATA_DIR:-~/meshcore-data}:/app/data
- caddy-data:/data/caddy
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
timeout: 5s
retries: 3
staging:
build: .
image: corescope:latest
container_name: corescope-staging
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${STAGING_HTTP_PORT:-81}:${STAGING_HTTP_PORT:-81}"
- "${STAGING_MQTT_PORT:-1884}:1883"
volumes:
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}/config.json:/app/config.json:ro
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}/Caddyfile:/etc/caddy/Caddyfile:ro
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}:/app/data
- caddy-data-staging:/data/caddy
environment:
- NODE_ENV=staging
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
timeout: 5s
retries: 3
profiles:
- staging
staging-go:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
image: corescope-go:latest
container_name: corescope-staging-go
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${STAGING_GO_HTTP_PORT:-82}:80"
- "${STAGING_GO_MQTT_PORT:-1885}:1883"
- "6060:6060" # pprof server
- "6061:6061" # pprof ingestor
volumes:
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}/config.json:/app/config.json:ro
- ${STAGING_DATA_DIR:-~/meshcore-staging-data}:/app/data
- caddy-data-staging-go:/data/caddy
environment:
- NODE_ENV=staging
- ENABLE_PPROF=true
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
timeout: 5s
retries: 3
profiles:
- staging-go
volumes:
# Named volumes for Caddy TLS certificates (not user data — managed by Caddy internally)
caddy-data:
caddy-data-staging:
caddy-data-staging-go:

View File

@@ -18,12 +18,6 @@ STATE_FILE=".setup-state"
# Resolved paths for prod/staging data (must match docker-compose.yml)
PROD_DATA="${PROD_DATA_DIR:-$HOME/meshcore-data}"
STAGING_DATA="${STAGING_DATA_DIR:-$HOME/meshcore-staging-data}"
STAGING_COMPOSE_FILE="docker-compose.staging.yml"
# Build metadata — exported so docker compose build picks them up via args
export APP_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
export GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
export BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Colors
RED='\033[0;31m'
@@ -392,7 +386,7 @@ prepare_staging_db() {
# Copy config.prod.json → config.staging.json with siteName change
prepare_staging_config() {
local prod_config="./config.json"
local prod_config="$PROD_DATA/config.json"
local staging_config="$STAGING_DATA/config.json"
if [ ! -f "$prod_config" ]; then
warn "No config.json found at ${prod_config} — staging may not start correctly."
@@ -441,11 +435,10 @@ cmd_start() {
prepare_staging_config
info "Starting production container (corescope-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
info "Starting staging container (corescope-staging-go) on port ${STAGING_GO_HTTP_PORT:-82}..."
docker compose up -d prod
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging up -d staging-go
info "Starting staging container (corescope-staging) on port ${STAGING_HTTP_PORT:-81}..."
docker compose --profile staging up -d
log "Production started on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}/${PROD_MQTT_PORT:-1883}"
log "Staging started on port ${STAGING_GO_HTTP_PORT:-82} (MQTT: ${STAGING_GO_MQTT_PORT:-1885})"
log "Staging started on port ${STAGING_HTTP_PORT:-81} (MQTT: ${STAGING_MQTT_PORT:-1884})"
else
info "Starting production container (corescope-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
docker compose up -d prod
@@ -463,16 +456,13 @@ cmd_stop() {
log "Production stopped."
;;
staging)
info "Stopping staging container (corescope-staging-go)..."
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging rm -sf staging-go 2>/dev/null || true
docker rm -f corescope-staging-go meshcore-staging-go corescope-staging meshcore-staging 2>/dev/null || true
log "Staging stopped and cleaned up."
info "Stopping staging container (corescope-staging)..."
docker compose --profile staging stop staging
log "Staging stopped."
;;
all)
info "Stopping all containers..."
docker compose stop prod
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging rm -sf staging-go 2>/dev/null || true
docker rm -f corescope-staging-go meshcore-staging-go corescope-staging meshcore-staging 2>/dev/null || true
docker compose --profile staging --profile staging-go down
log "All containers stopped."
;;
*)
@@ -491,18 +481,13 @@ cmd_restart() {
log "Production restarted."
;;
staging)
info "Restarting staging container (corescope-staging-go)..."
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging rm -sf staging-go 2>/dev/null || true
docker rm -f corescope-staging-go 2>/dev/null || true
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging up -d staging-go
info "Restarting staging container (corescope-staging)..."
docker compose --profile staging up -d --force-recreate staging
log "Staging restarted."
;;
all)
info "Restarting all containers..."
docker compose up -d --force-recreate prod
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging rm -sf staging-go 2>/dev/null || true
docker rm -f corescope-staging-go 2>/dev/null || true
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging up -d staging-go
docker compose --profile staging up -d --force-recreate
log "All containers restarted."
;;
*)
@@ -554,10 +539,10 @@ cmd_status() {
echo ""
# Staging
if container_running "corescope-staging-go"; then
show_container_status "corescope-staging-go" "Staging"
if container_running "corescope-staging"; then
show_container_status "corescope-staging" "Staging"
else
info "Staging (corescope-staging-go): Not running (use --with-staging to start both)"
info "Staging (corescope-staging): Not running (use --with-staging to start both)"
fi
echo ""
@@ -589,7 +574,7 @@ cmd_logs() {
staging)
if container_running "corescope-staging"; then
info "Tailing staging logs..."
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging logs -f --tail="$LINES" staging-go
docker compose logs -f --tail="$LINES" staging
else
err "Staging container is not running."
info "Start with: ./manage.sh start --with-staging"
@@ -617,7 +602,7 @@ cmd_promote() {
# Show what's currently running
local staging_image staging_created prod_image prod_created
staging_image=$(docker inspect corescope-staging-go --format '{{.Config.Image}}' 2>/dev/null || echo "not running")
staging_image=$(docker inspect corescope-staging --format '{{.Config.Image}}' 2>/dev/null || echo "not running")
staging_created=$(docker inspect corescope-staging --format '{{.Created}}' 2>/dev/null || echo "N/A")
prod_image=$(docker inspect corescope-prod --format '{{.Config.Image}}' 2>/dev/null || echo "not running")
prod_created=$(docker inspect corescope-prod --format '{{.Created}}' 2>/dev/null || echo "N/A")
@@ -863,8 +848,7 @@ cmd_reset() {
exit 0
fi
docker compose down --rmi local 2>/dev/null || true
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging down --rmi local 2>/dev/null || true
docker compose --profile staging --profile staging-go down --rmi local 2>/dev/null || true
rm -f "$STATE_FILE"
log "Reset complete. Run './manage.sh setup' to start over."
@@ -885,7 +869,7 @@ cmd_help() {
echo ""
printf '%b\n' " ${BOLD}Run${NC}"
echo " start Start production container"
echo " start --with-staging Start production + staging-go (copies prod DB + config)"
echo " start --with-staging Start production + staging (copies prod DB + config)"
echo " stop [prod|staging|all] Stop specific or all containers (default: all)"
echo " restart [prod|staging|all] Restart specific or all containers"
echo " status Show health, stats, and service status"
@@ -898,7 +882,7 @@ cmd_help() {
echo " restore <d> Restore from backup dir or .db file"
echo " mqtt-test Check if MQTT data is flowing"
echo ""
echo "Prod uses docker-compose.yml; staging uses ${STAGING_COMPOSE_FILE}."
echo "All commands use docker compose with docker-compose.yml."
echo ""
}

View File

@@ -876,26 +876,6 @@
</div>`;
}).join('')}
</div>
${data.distributionByRepeaters ? (() => {
const dr = data.distributionByRepeaters;
const totalRepeaters = (dr[1] || 0) + (dr[2] || 0) + (dr[3] || 0);
const rpct = (n) => totalRepeaters ? (n / totalRepeaters * 100).toFixed(1) : '0';
const maxRepeaters = Math.max(dr[1] || 0, dr[2] || 0, dr[3] || 0, 1);
const colors = { 1: '#ef4444', 2: '#22c55e', 3: '#3b82f6' };
return `<h4 style="margin:16px 0 4px">By Repeaters</h4>
<p class="text-muted">${totalRepeaters.toLocaleString()} unique repeaters</p>
<div class="hash-bars">
${[1, 2, 3].map(size => {
const count = dr[size] || 0;
const width = Math.max((count / maxRepeaters) * 100, count ? 2 : 0);
return `<div class="hash-bar-row">
<div class="hash-bar-label"><strong>${size}-byte</strong></div>
<div class="hash-bar-track"><div class="hash-bar-fill" style="width:${width}%;background:${colors[size]};opacity:0.7"></div></div>
<div class="hash-bar-value">${count.toLocaleString()} <span class="text-muted">(${rpct(count)}%)</span></div>
</div>`;
}).join('')}
</div>`;
})() : ''}
</div>
<div class="analytics-card flex-1">
<h3>📈 Hash Size Over Time</h3>

View File

@@ -22,9 +22,9 @@
<meta name="twitter:title" content="CoreScope">
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774821783">
<link rel="stylesheet" href="home.css?v=1774821783">
<link rel="stylesheet" href="live.css?v=1774821783">
<link rel="stylesheet" href="style.css?v=1774786038">
<link rel="stylesheet" href="home.css?v=1774786038">
<link rel="stylesheet" href="live.css?v=1774786038">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -81,29 +81,29 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1774821783"></script>
<script src="customize.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774821783"></script>
<script src="hop-resolver.js?v=1774821783"></script>
<script src="hop-display.js?v=1774821783"></script>
<script src="app.js?v=1774821783"></script>
<script src="home.js?v=1774821783"></script>
<script src="packet-filter.js?v=1774821783"></script>
<script src="packets.js?v=1774821783"></script>
<script src="map.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774821783" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1774786038"></script>
<script src="customize.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774786038"></script>
<script src="hop-resolver.js?v=1774786038"></script>
<script src="hop-display.js?v=1774786038"></script>
<script src="app.js?v=1774786038"></script>
<script src="home.js?v=1774786038"></script>
<script src="packet-filter.js?v=1774786038"></script>
<script src="packets.js?v=1774786038"></script>
<script src="map.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774786038" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>

View File

@@ -1987,17 +1987,9 @@ app.get('/api/analytics/hash-sizes', (req, res) => {
.sort(([, a], [, b]) => b.packets - a.packets)
.map(([name, data]) => ({ name, ...data }));
// Distribution by number of repeaters advertising each hash size
const distributionByRepeaters = { 1: 0, 2: 0, 3: 0 };
for (const [, v] of Object.entries(byNode)) {
const s = v.hashSize;
if (s >= 1 && s <= 3) distributionByRepeaters[s]++;
}
const _hsResult = {
total: packets.length,
distribution,
distributionByRepeaters,
hourly,
topHops,
multiByteNodes

View File

@@ -583,11 +583,6 @@ seedTestData();
await t('GET /api/analytics/hash-sizes', async () => {
const r = await request(app).get('/api/analytics/hash-sizes').expect(200);
assert(typeof r.body === 'object', 'should return hash sizes');
assert(r.body.distributionByRepeaters, 'should include distributionByRepeaters');
assert(typeof r.body.distributionByRepeaters === 'object', 'distributionByRepeaters should be an object');
for (const s of [1, 2, 3]) {
assert(typeof r.body.distributionByRepeaters[s] === 'number', `distributionByRepeaters[${s}] should be a number`);
}
});
await t('GET /api/analytics/hash-sizes with region', async () => {