mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 15:55:49 +00:00
Compare commits
1 Commits
feature/ha
...
perf/cover
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f253c29d6e |
1
.badges/backend-coverage.json
Normal file
1
.badges/backend-coverage.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"label":"backend coverage","message":"87.79%","color":"brightgreen"}
|
||||
1
.badges/backend-tests.json
Normal file
1
.badges/backend-tests.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"label":"backend tests","message":"998 passed","color":"brightgreen"}
|
||||
1
.badges/coverage.json
Normal file
1
.badges/coverage.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"label":"coverage","message":"76%","color":"yellow"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"label":"e2e tests","message":"pending","color":"lightgrey"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"label":"go ingestor coverage","message":"pending","color":"lightgrey"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"label":"go server coverage","message":"pending","color":"lightgrey"}
|
||||
1
.badges/tests.json
Normal file
1
.badges/tests.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"label":"tests","message":"844/844 passed","color":"brightgreen"}
|
||||
266
.github/workflows/deploy.yml
vendored
266
.github/workflows/deploy.yml
vendored
@@ -1,28 +1,35 @@
|
||||
name: CI/CD Pipeline
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
group: deploy-${{ 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.
|
||||
# Pipeline:
|
||||
# node-test (frontend tests) ──┐
|
||||
# go-test ├──→ build → deploy → publish
|
||||
# └─ (both wait)
|
||||
#
|
||||
# Proto validation flow:
|
||||
# 1. go-test job: verify .proto files compile (syntax check)
|
||||
# 2. deploy job: capture fresh fixtures from prod, validate protos match actual API responses
|
||||
|
||||
jobs:
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
# 1. Go Build & Test
|
||||
# 1. Go Build & Test — compiles + tests Go modules, coverage badges
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
go-test:
|
||||
name: "✅ Go Build & Test"
|
||||
@@ -33,10 +40,20 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Clean Go module cache
|
||||
run: rm -rf ~/go/pkg/mod 2>/dev/null || true
|
||||
- name: Skip if docs-only change
|
||||
id: docs-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||
if [ "$NON_DOCS" -eq 0 ]; then
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
echo "📄 Docs-only PR — skipping heavy CI"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Set up Go 1.22
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
@@ -45,6 +62,7 @@ jobs:
|
||||
cmd/ingestor/go.sum
|
||||
|
||||
- name: Build and test Go server (with coverage)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
cd cmd/server
|
||||
@@ -54,6 +72,7 @@ jobs:
|
||||
go tool cover -func=server-coverage.out | tail -1
|
||||
|
||||
- name: Build and test Go ingestor (with coverage)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
cd cmd/ingestor
|
||||
@@ -62,11 +81,15 @@ jobs:
|
||||
echo "--- Go Ingestor Coverage ---"
|
||||
go tool cover -func=ingestor-coverage.out | tail -1
|
||||
|
||||
- name: Verify proto syntax
|
||||
- name: Verify proto syntax (all .proto files compile)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e
|
||||
echo "Installing protoc..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y protobuf-compiler
|
||||
|
||||
echo "Checking proto syntax..."
|
||||
for proto in proto/*.proto; do
|
||||
echo " ✓ $(basename "$proto")"
|
||||
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
|
||||
@@ -74,27 +97,37 @@ jobs:
|
||||
echo "✅ All .proto files are syntactically valid"
|
||||
|
||||
- name: Generate Go coverage badges
|
||||
if: success()
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
mkdir -p .badges
|
||||
|
||||
# Parse server coverage
|
||||
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
|
||||
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
|
||||
echo "Go server coverage: ${SERVER_COV}% (${SERVER_COLOR})"
|
||||
|
||||
# Parse ingestor coverage
|
||||
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
|
||||
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 ingestor coverage: ${INGESTOR_COV}% (${INGESTOR_COLOR})"
|
||||
|
||||
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -102,9 +135,16 @@ jobs:
|
||||
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Cancel workflow on failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
||||
|
||||
- name: Upload Go coverage badges
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: go-badges
|
||||
path: .badges/go-*.json
|
||||
@@ -112,11 +152,10 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
# 2. Playwright E2E Tests (against Go server with fixture DB)
|
||||
# 2. Node.js Tests — backend unit tests + Playwright E2E, coverage
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
e2e-test:
|
||||
name: "🎭 Playwright E2E Tests"
|
||||
needs: [go-test]
|
||||
node-test:
|
||||
name: "🧪 Node.js Tests"
|
||||
runs-on: [self-hosted, Linux]
|
||||
defaults:
|
||||
run:
|
||||
@@ -127,43 +166,102 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Skip if docs-only change
|
||||
id: docs-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||
if [ "$NON_DOCS" -eq 0 ]; then
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
echo "📄 Docs-only PR — skipping heavy CI"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Set up Node.js 22
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
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
|
||||
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
|
||||
- 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
|
||||
|
||||
- name: Install Playwright browser
|
||||
- name: Detect changed files
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
id: changes
|
||||
run: |
|
||||
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
||||
FRONTEND=$(git diff --name-only HEAD~1 | grep -cE '^public/' || true)
|
||||
TESTS=$(git diff --name-only HEAD~1 | grep -cE '^test-|^tools/' || true)
|
||||
CI=$(git diff --name-only HEAD~1 | grep -cE '\.github/|package\.json|test-all\.sh|scripts/' || true)
|
||||
# If CI/test infra changed, run everything
|
||||
if [ "$CI" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
||||
# If test files changed, run everything
|
||||
if [ "$TESTS" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
||||
echo "backend=$([[ $BACKEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
echo "frontend=$([[ $FRONTEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
||||
|
||||
- name: Run backend tests with coverage
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'true'
|
||||
run: |
|
||||
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
||||
|
||||
TOTAL_PASS=$(grep -oP '\d+(?= passed)' test-output.txt | awk '{s+=$1} END {print s}')
|
||||
TOTAL_FAIL=$(grep -oP '\d+(?= failed)' test-output.txt | awk '{s+=$1} END {print s}')
|
||||
BE_COVERAGE=$(grep 'Statements' test-output.txt | tail -1 | grep -oP '[\d.]+(?=%)')
|
||||
|
||||
mkdir -p .badges
|
||||
BE_COLOR="red"
|
||||
[ "$(echo "$BE_COVERAGE > 60" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="yellow"
|
||||
[ "$(echo "$BE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="brightgreen"
|
||||
echo "{\"schemaVersion\":1,\"label\":\"backend tests\",\"message\":\"${TOTAL_PASS} passed\",\"color\":\"brightgreen\"}" > .badges/backend-tests.json
|
||||
echo "{\"schemaVersion\":1,\"label\":\"backend coverage\",\"message\":\"${BE_COVERAGE}%\",\"color\":\"${BE_COLOR}\"}" > .badges/backend-coverage.json
|
||||
|
||||
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Run backend tests (quick, no coverage)
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'false'
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Install Playwright browser
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
# Install chromium (skips download if already cached on self-hosted runner)
|
||||
npx playwright install chromium 2>/dev/null || true
|
||||
# Install system deps only if missing (apt-get is slow)
|
||||
npx playwright install-deps chromium 2>/dev/null || true
|
||||
|
||||
- name: Instrument frontend JS for coverage
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: sh scripts/instrument-frontend.sh
|
||||
|
||||
- name: Start Go server with fixture DB
|
||||
- name: Start instrumented test server on port 13581
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
# Kill any stale server on 13581
|
||||
fuser -k 13581/tcp 2>/dev/null || true
|
||||
sleep 1
|
||||
./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public-instrumented &
|
||||
sleep 2
|
||||
./corescope-server -port 13581 -public public-instrumented &
|
||||
echo $! > .server.pid
|
||||
echo "Server PID: $(cat .server.pid)"
|
||||
# Health-check poll loop (up to 30s)
|
||||
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"
|
||||
@@ -171,27 +269,38 @@ jobs:
|
||||
fi
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Playwright E2E tests (fail-fast)
|
||||
- name: Run Playwright E2E + coverage collection concurrently
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
|
||||
# Run E2E tests and coverage collection in parallel — both use the same server
|
||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt &
|
||||
E2E_PID=$!
|
||||
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt &
|
||||
COV_PID=$!
|
||||
|
||||
# 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
|
||||
# Wait for both — E2E must pass, coverage is best-effort
|
||||
E2E_EXIT=0
|
||||
wait $E2E_PID || E2E_EXIT=$?
|
||||
wait $COV_PID || true
|
||||
|
||||
# Fail if E2E failed
|
||||
[ $E2E_EXIT -ne 0 ] && exit $E2E_EXIT
|
||||
true
|
||||
|
||||
- name: Generate frontend coverage badges
|
||||
if: success()
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1 || echo "0")
|
||||
|
||||
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
||||
|
||||
mkdir -p .badges
|
||||
# Merge E2E + coverage collector data if both exist
|
||||
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")
|
||||
@@ -202,21 +311,43 @@ jobs:
|
||||
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
|
||||
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
||||
|
||||
- name: Stop test server
|
||||
if: success()
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
if [ -f .server.pid ]; then
|
||||
kill $(cat .server.pid) 2>/dev/null || true
|
||||
rm -f .server.pid
|
||||
echo "Server stopped"
|
||||
fi
|
||||
|
||||
- name: Upload E2E badges
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v6
|
||||
- name: Run frontend E2E (quick, no coverage)
|
||||
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 &
|
||||
SERVER_PID=$!
|
||||
# Wait for server to be ready (up to 15s)
|
||||
for i in $(seq 1 15); do
|
||||
curl -sf http://localhost:13581/api/stats > /dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
|
||||
- name: Cancel workflow on failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
||||
|
||||
- name: Upload Node.js test badges
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: e2e-badges
|
||||
name: node-badges
|
||||
path: .badges/
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
@@ -226,7 +357,8 @@ jobs:
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: "🏗️ Build Docker Image"
|
||||
needs: [e2e-test]
|
||||
if: github.event_name == 'push'
|
||||
needs: [go-test, node-test]
|
||||
runs-on: [self-hosted, Linux]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -242,15 +374,11 @@ jobs:
|
||||
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 ✅"
|
||||
docker compose --profile staging-go build staging-go
|
||||
echo "Built Go staging image"
|
||||
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
# 4. Deploy Staging (master only)
|
||||
# 4. Deploy Staging — start on port 82, healthcheck, smoke test
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: "🚀 Deploy Staging"
|
||||
@@ -261,8 +389,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Deploy staging
|
||||
run: ./manage.sh restart staging
|
||||
- name: Start staging on port 82
|
||||
run: |
|
||||
# Force remove stale containers
|
||||
docker rm -f corescope-staging-go 2>/dev/null || true
|
||||
# Clean up stale ports
|
||||
fuser -k 82/tcp 2>/dev/null || true
|
||||
docker compose --profile staging-go up -d staging-go
|
||||
|
||||
- name: Healthcheck staging container
|
||||
run: |
|
||||
@@ -290,7 +423,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
# 5. Publish Badges & Summary (master only)
|
||||
# 5. Publish Badges & Summary
|
||||
# ───────────────────────────────────────────────────────────────
|
||||
publish:
|
||||
name: "📝 Publish Badges & Summary"
|
||||
@@ -303,16 +436,16 @@ jobs:
|
||||
|
||||
- name: Download Go coverage badges
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: go-badges
|
||||
path: .badges/
|
||||
|
||||
- name: Download E2E badges
|
||||
- name: Download Node.js test badges
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: e2e-badges
|
||||
name: node-badges
|
||||
path: .badges/
|
||||
|
||||
- name: Publish coverage badges to repo
|
||||
@@ -329,3 +462,12 @@ jobs:
|
||||
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
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Staging:** http://<VM_HOST>:82" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "To promote to production:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||
echo "ssh deploy@\$VM_HOST" >> $GITHUB_STEP_SUMMARY
|
||||
echo "cd /opt/corescope-deploy" >> $GITHUB_STEP_SUMMARY
|
||||
echo "./manage.sh promote" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,4 +28,3 @@ reps.txt
|
||||
cmd/server/server.exe
|
||||
cmd/ingestor/ingestor.exe
|
||||
# CI trigger
|
||||
!test-fixtures/e2e-fixture.db
|
||||
|
||||
10
.nycrc.json
10
.nycrc.json
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"include": [
|
||||
"public/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"public/vendor/**",
|
||||
"public/leaflet-*.js",
|
||||
"public/qrcode*.js"
|
||||
]
|
||||
}
|
||||
12
README.md
12
README.md
@@ -1,10 +1,10 @@
|
||||
# CoreScope
|
||||
|
||||
[](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/CoreScope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
|
||||
[](https://github.com/Kpa-clawbot/corescope/actions/workflows/deploy.yml)
|
||||
[](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
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
|
||||
54
manage.sh
54
manage.sh
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Capture a fixture DB from staging for E2E tests
|
||||
# Usage: ./scripts/capture-fixture.sh [source_url]
|
||||
#
|
||||
# Downloads nodes, observers, and recent packets from the staging API
|
||||
# and creates a SQLite database suitable for E2E testing.
|
||||
|
||||
set -e
|
||||
|
||||
SOURCE_URL="${1:-https://analyzer.00id.net}"
|
||||
DB_PATH="test-fixtures/e2e-fixture.db"
|
||||
|
||||
echo "Capturing fixture from $SOURCE_URL..."
|
||||
|
||||
mkdir -p test-fixtures
|
||||
rm -f "$DB_PATH"
|
||||
|
||||
# Create schema
|
||||
sqlite3 "$DB_PATH" <<'SQL'
|
||||
CREATE TABLE nodes (
|
||||
public_key TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
role TEXT,
|
||||
lat REAL,
|
||||
lon REAL,
|
||||
last_seen TEXT,
|
||||
first_seen TEXT,
|
||||
advert_count INTEGER DEFAULT 0,
|
||||
battery_mv INTEGER,
|
||||
temperature_c REAL
|
||||
);
|
||||
|
||||
CREATE TABLE observers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
iata TEXT,
|
||||
last_seen TEXT,
|
||||
first_seen TEXT,
|
||||
packet_count INTEGER DEFAULT 0,
|
||||
model TEXT,
|
||||
firmware TEXT,
|
||||
client_version TEXT,
|
||||
radio TEXT,
|
||||
battery_mv INTEGER,
|
||||
uptime_secs INTEGER,
|
||||
noise_floor REAL
|
||||
);
|
||||
|
||||
CREATE TABLE transmissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
raw_hex TEXT NOT NULL,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
first_seen TEXT NOT NULL,
|
||||
route_type INTEGER,
|
||||
payload_type INTEGER,
|
||||
payload_version INTEGER,
|
||||
decoded_json TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transmission_id INTEGER NOT NULL REFERENCES transmissions(id),
|
||||
observer_idx INTEGER,
|
||||
direction TEXT,
|
||||
snr REAL,
|
||||
rssi REAL,
|
||||
score INTEGER,
|
||||
path_json TEXT,
|
||||
timestamp INTEGER NOT NULL
|
||||
);
|
||||
SQL
|
||||
|
||||
# Fetch nodes
|
||||
echo "Fetching nodes..."
|
||||
curl -sf "$SOURCE_URL/api/nodes?limit=200" | python3 -c "
|
||||
import json, sys, sqlite3
|
||||
data = json.load(sys.stdin)
|
||||
nodes = data.get('nodes', data) if isinstance(data, dict) else data
|
||||
db = sqlite3.connect('$DB_PATH')
|
||||
for n in nodes[:200]:
|
||||
db.execute('INSERT OR IGNORE INTO nodes VALUES (?,?,?,?,?,?,?,?,?,?)',
|
||||
(n.get('public_key',''), n.get('name',''), n.get('role',''),
|
||||
n.get('lat'), n.get('lon'), n.get('last_seen',''), n.get('first_seen',''),
|
||||
n.get('advert_count',0), n.get('battery_mv'), n.get('temperature_c')))
|
||||
db.commit()
|
||||
print(f' Inserted {min(len(nodes), 200)} nodes')
|
||||
db.close()
|
||||
"
|
||||
|
||||
# Fetch observers
|
||||
echo "Fetching observers..."
|
||||
curl -sf "$SOURCE_URL/api/observers" | python3 -c "
|
||||
import json, sys, sqlite3
|
||||
data = json.load(sys.stdin)
|
||||
observers = data.get('observers', data) if isinstance(data, dict) else data
|
||||
db = sqlite3.connect('$DB_PATH')
|
||||
for o in observers:
|
||||
db.execute('INSERT OR IGNORE INTO observers VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)',
|
||||
(o.get('id',''), o.get('name',''), o.get('iata',''),
|
||||
o.get('last_seen',''), o.get('first_seen',''),
|
||||
o.get('packet_count',0), o.get('model',''), o.get('firmware',''),
|
||||
o.get('client_version',''), o.get('radio',''),
|
||||
o.get('battery_mv'), o.get('uptime_secs'), o.get('noise_floor')))
|
||||
db.commit()
|
||||
print(f' Inserted {len(observers)} observers')
|
||||
db.close()
|
||||
"
|
||||
|
||||
# Fetch recent packets
|
||||
echo "Fetching recent packets..."
|
||||
curl -sf "$SOURCE_URL/api/packets?limit=500" | python3 -c "
|
||||
import json, sys, sqlite3
|
||||
data = json.load(sys.stdin)
|
||||
packets = data.get('packets', data) if isinstance(data, dict) else data
|
||||
db = sqlite3.connect('$DB_PATH')
|
||||
for p in packets:
|
||||
try:
|
||||
cur = db.execute('INSERT OR IGNORE INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, payload_version, decoded_json) VALUES (?,?,?,?,?,?,?)',
|
||||
(p.get('raw_hex',''), p.get('hash',''), p.get('first_seen',''),
|
||||
p.get('route_type'), p.get('payload_type'), p.get('payload_version'),
|
||||
p.get('decoded_json')))
|
||||
tid = cur.lastrowid
|
||||
if tid and p.get('observer_id'):
|
||||
db.execute('INSERT INTO observations (transmission_id, observer_idx, direction, snr, rssi, score, path_json, timestamp) VALUES (?,?,?,?,?,?,?,?)',
|
||||
(tid, p.get('observer_id'), p.get('direction'),
|
||||
p.get('snr'), p.get('rssi'), None,
|
||||
p.get('path_json'),
|
||||
int(p.get('timestamp','0')) if p.get('timestamp','').isdigit() else 0))
|
||||
except Exception as e:
|
||||
pass # Skip duplicates
|
||||
db.commit()
|
||||
print(f' Inserted {len(packets)} transmissions')
|
||||
db.close()
|
||||
"
|
||||
|
||||
SIZE=$(du -h "$DB_PATH" | cut -f1)
|
||||
echo "✅ Fixture DB created: $DB_PATH ($SIZE)"
|
||||
echo " Nodes: $(sqlite3 "$DB_PATH" 'SELECT COUNT(*) FROM nodes')"
|
||||
echo " Observers: $(sqlite3 "$DB_PATH" 'SELECT COUNT(*) FROM observers')"
|
||||
echo " Transmissions: $(sqlite3 "$DB_PATH" 'SELECT COUNT(*) FROM transmissions')"
|
||||
echo " Observations: $(sqlite3 "$DB_PATH" 'SELECT COUNT(*) FROM observations')"
|
||||
@@ -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
|
||||
|
||||
@@ -17,8 +17,6 @@ async function test(name, fn) {
|
||||
} catch (err) {
|
||||
results.push({ name, pass: false, error: err.message });
|
||||
console.log(` \u274c ${name}: ${err.message}`);
|
||||
console.log(`\nFail-fast: stopping after first failure.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,9 +351,7 @@ async function run() {
|
||||
});
|
||||
|
||||
// Test: Clicking a packet row opens detail pane
|
||||
// SKIPPED: flaky test — see https://github.com/Kpa-clawbot/CoreScope/issues/257
|
||||
console.log(' ⏭️ Packets clicking row shows detail pane (SKIPPED — flaky)');
|
||||
/*await test('Packets clicking row shows detail pane', async () => {
|
||||
await test('Packets clicking row shows detail pane', async () => {
|
||||
// Fresh navigation to avoid stale row references from previous test
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
// Wait for table rows AND initial API data to settle
|
||||
@@ -410,8 +406,7 @@ async function run() {
|
||||
}, { timeout: 3000 });
|
||||
const panelHidden = await page.$eval('#pktRight', el => el.classList.contains('empty'));
|
||||
assert(panelHidden, 'Detail pane should be hidden after clicking ✕');
|
||||
});*/
|
||||
console.log(' ⏭️ Packet detail pane closes on ✕ click (SKIPPED — depends on flaky test above)');
|
||||
});
|
||||
|
||||
// Test: GRP_TXT packet detail shows Channel Hash (#123)
|
||||
await test('GRP_TXT packet detail shows Channel Hash', async () => {
|
||||
|
||||
Binary file not shown.
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user