Compare commits

...

22 Commits

Author SHA1 Message Date
Kpa-clawbot a1a67e89fb feat: manage.sh reads .env for data paths — consistent with docker compose
- Replace all hardcoded \C:\Users\KpaBap/meshcore-data with \ variable
- \ resolves from \ in .env or defaults to ~/meshcore-data
- Updated get_data_mount_args(), cmd_backup(), cmd_restore(), cmd_reset()
- Enhanced .env.example with detailed comments for each variable
- Both docker compose and manage.sh now read same .env file

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 16:06:13 -07:00
Kpa-clawbot 91fcbc5adc Fix: Use bind mounts instead of named volumes for data directory
PROBLEM:
manage.sh was using named Docker volumes (meshcore-data) as the default,
which hides the database and theme files inside Docker's internal storage.
Users couldn't find their DB on the filesystem for backups or inspection.

The function get_data_mount_args() had conditional logic that only used
bind mounts IF it detected an existing ~/meshcore-data with a DB file.
For new installs, it fell through to the named volume — silently hiding
all data in /var/lib/docker/volumes/.

FIXES:
1. get_data_mount_args() — Always use bind mount to ~/meshcore-data
   - Creates the directory if it doesn't exist
   - Removes all conditional logic and the named volume fallback

2. cmd_backup() — Use direct path C:\Users\KpaBap/meshcore-data/meshcore.db
   - No longer tries to inspect the named volume
   - Consistent with the bind mount approach

3. cmd_restore() — Use direct path for restore operations
   - Ensures directory exists before restoring files
   - No fallback to docker cp

4. cmd_reset() — Updated message to reflect bind mount location
   - Changed from 'docker volume rm' to '~/meshcore-data (not removed)'

5. docker-compose.yml — Added documentation comment
   - Clarifies that bind mounts are intentional, not named volumes
   - Ensures future changes maintain this pattern

VALIDATION:
- docker-compose.yml already used bind mounts correctly (\)
- Legacy 'docker run' mode now matches compose behavior
- All backup/restore operations reference the same bind mount path

DATABASE LOCATION:
- Always: ~/meshcore-data/meshcore.db
- Never: Hidden in Docker's volume storage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Requested-by: Kpa-clawbot
2026-03-28 16:01:16 -07:00
efiten 380b1b1e28 fix: address review — observation ordering, stale comments, affected query functions
- Load() SQL: keep o.timestamp DESC (consistent with IngestNewFromDB) so
  pickBestObservation tie-breaking is identical on both load paths
- GetTimestamps: scan from tail instead of head (was breaking on first item
  assuming it was the newest, now correctly reads from newest end)
- QueryMultiNodePackets: apply same DESC/ASC tail-read pagination as
  QueryPackets (was sorting for ASC and assuming DESC as-is)
- GetNodeHealth recentPackets: read from tail to return 20 newest items
  (was reading from head = 20 oldest items)
- Remove stale "Prepend (newest first)" comments, replace with accurate
  "oldest-first; new items go to tail" wording

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:54:40 -07:00
efiten 03cfd114da perf: eliminate O(n) slice prepend on every packet ingest
s.packets and s.byPayloadType[t] were prepended on every new packet
to maintain newest-first order, copying the entire slice each time.
With 2-3M packets in memory this meant ~24MB of pointer copies per
ingest cycle, causing sustained high CPU and GC pressure.

Fix: store both slices oldest-first (append to tail). Load() SQL
changed to ASC ordering. QueryPackets DESC pagination now reads from
the tail in O(page_size) with no sort; GetChannelMessages switches
from reverse-iteration to forward-iteration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:54:40 -07:00
Kpa-clawbot df90de77a7 Merge pull request #219 from Kpa-clawbot/fix/hashchannels-derivation
fix: port hashChannels key derivation to Go ingestor (fixes #218)
2026-03-28 15:34:43 -07:00
copilot-swe-agent[bot] 7b97c532a1 test: fix env isolation and comment accuracy in channel key tests
Agent-Logs-Url: https://github.com/Kpa-clawbot/meshcore-analyzer/sessions/38b3e96f-861b-4929-8134-b1b9de39a7fc

Co-authored-by: KpaBap <746025+KpaBap@users.noreply.github.com>
2026-03-28 15:27:26 -07:00
Kpa-clawbot e0c2d37041 fix: port hashChannels key derivation to Go ingestor (fixes #218)
Add HashChannels config field and deriveHashtagChannelKey() to the Go
ingestor, matching the Node.js server-helpers.js algorithm:
SHA-256(channelName) -> first 32 hex chars (16 bytes AES-128 key).

Merge priority preserved: rainbow (lowest) -> derived -> explicit (highest).

Tests include cross-language vectors validated against Node.js output
and merge priority / normalization / skip-explicit coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 15:27:26 -07:00
Kpa-clawbot f5d0ce066b refactor: remove packets_v SQL fallbacks — store handles all queries (#220)
* refactor: remove all packets_v SQL fallbacks — store handles all queries

Remove DB fallback paths from all route handlers. The in-memory
PacketStore now handles all packet/node/analytics queries. Handlers
return empty results or 404 when no store is available instead of
falling back to direct DB queries.

- Remove else-DB branches from handlePacketDetail, handleNodeHealth,
  handleNodeAnalytics, handleBulkHealth, handlePacketTimestamps, etc.
- Remove unused DB methods (GetPacketByHash, GetTransmissionByID,
  GetPacketByID, GetObservationsForHash, GetTimestamps, GetNodeHealth,
  GetNodeAnalytics, GetBulkHealth, etc.)
- Remove packets_v VIEW creation from schema
- Update tests for new behavior (no-store returns 404/empty, not 500)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR #220 review comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: KpaBap <kpabap@gmail.com>
2026-03-28 15:25:56 -07:00
Kpa-clawbot 202d0d87d7 ci: Add pull_request trigger to CI workflow
- Add pull_request trigger for PRs against master
- Add 'if: github.event_name == push' to build/deploy/publish jobs
- Test jobs (go-test, node-test) now run on both push and PRs
- Build/deploy/publish only run on push to master

This fixes the chicken-and-egg problem where branch protection requires
CI checks but CI doesn't run on PRs. Now PRs get test validation before
merge while keeping production deployments only on master pushes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 15:15:35 -07:00
Kpa-clawbot 99d2e67eb1 Rename Phase 1: MeshCore Analyzer -> CoreScope (backend + infra)
Reviewed by Kobayashi (gpt-5.3-codex). All comments addressed.
2026-03-28 14:45:24 -07:00
Kpa-clawbot a6413fb665 fix: address review — stale URLs, manage.sh branding, proto comment
- docs/go-migration.md: update clone URL meshcore-dev/meshcore-analyzer → Kpa-clawbot/meshcore-analyzer
- manage.sh: rename header comment and help footer from 'MeshCore Analyzer' to 'CoreScope'
- proto/config.proto: update default branding comment from 'MeshCore Analyzer' to 'CoreScope'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 14:44:53 -07:00
KpaBap 8a458c7c2a Merge pull request #227 from Kpa-clawbot/rename/corescope-frontend
rename: MeshCore Analyzer → CoreScope (frontend + .squad)
2026-03-28 14:39:06 -07:00
Kpa-clawbot 66b3c05da3 fix: remove stray backtick in template literal
Fixes malformed template literal in test assertion message that would cause a syntax error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 14:37:27 -07:00
Kpa-clawbot cdcaa476f2 rename: MeshCore Analyzer → CoreScope (Phase 1 — backend + infra)
Rename product branding, binary names, Docker images, container names,
Go modules, proto go_package, CI, manage.sh, and documentation.

Preserved (backward compat):
- meshcore.db database filename
- meshcore-data / meshcore-staging-data directory paths
- MQTT topics (meshcore/#, meshcore/+/+/packets, etc.)
- proto package namespace (meshcore.v1)
- localStorage keys

Changes by category:
- Go modules: github.com/corescope/{server,ingestor}
- Binaries: corescope-server, corescope-ingestor
- Docker images: corescope:latest, corescope-go:latest
- Containers: corescope-prod, corescope-staging, corescope-staging-go
- Supervisord programs: corescope, corescope-server, corescope-ingestor
- Branding: siteName, heroTitle, startup logs, fallback HTML
- Proto go_package: github.com/corescope/proto/v1
- CI: container refs, deploy path
- Docs: 8 markdown files updated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 14:08:15 -07:00
Kpa-clawbot 71ec5e6fca rename: MeshCore Analyzer → CoreScope (frontend + .squad)
Phase 1 of the CoreScope rename — frontend display strings and
squad agent metadata only.

index.html:
- <title>, og:title, twitter:title → CoreScope
- Brand text span → CoreScope
- og:image/twitter:image URLs → corescope repo (placeholder)
- Cache busters bumped

public/*.js headers (19 files):
- All file header comments updated

public/*.css headers:
- style.css, home.css updated

JavaScript strings:
- app.js: GitHub URL → corescope
- home.js: 3 fallback siteName references
- customize.js: default siteName + heroTitle

Tests:
- test-e2e-playwright.js: title assertion → corescope
- test-frontend-helpers.js: GitHub URL constant
- benchmark.js: header string
- test-all.sh: header string

.squad:
- team.md, casting/history.json
- All 7 agent charters + 5 history files

NOT renamed (intentional):
- localStorage keys (meshcore-*)
- CSS classes (.meshcore-marker)
- Window globals (_meshcore*)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 14:03:32 -07:00
Kpa-clawbot a94c24c550 fix: restore PR reviewer instructions with valid filename (was *.instructions.md)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 14:02:14 -07:00
Kpa-clawbot a1f95fee58 fix: Dockerfile .git-commit COPY fails on legacy builder — use RUN default
The glob trick COPY .git-commi[t] only works with BuildKit.
manage.sh uses legacy docker build. Just create a default via RUN.
Commit hash comes through --build-arg ldflags anyway.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 13:59:20 -07:00
Kpa-clawbot 24d76f8373 fix: remove file with * in name — breaks Windows/NTFS 2026-03-28 13:57:31 -07:00
KpaBap 8e18351c73 Merge pull request #221 from Kpa-clawbot/feat/telemetry-decode
feat: decode telemetry packets — battery voltage + temperature on nodes
2026-03-28 13:45:00 -07:00
KpaBap 467a307a8d Create MeshCore PR Reviewer instructions
Added instructions for the MeshCore PR Reviewer agent, detailing its role, core principles, review focus areas, and the review process.
2026-03-28 13:26:23 -07:00
KpaBap 077fca9038 Create MeshCore PR Reviewer agent
Added a new agent for reviewing pull requests in the meshcore-analyzer repository, focusing on best practices and code quality.
2026-03-28 13:16:03 -07:00
Kpa-clawbot b326e3f1a6 fix: pprof port conflict crashed Go server — non-fatal bind + separate ports
Server defaults to 6060, ingestor to 6061. Removed shared PPROF_PORT
env var. Bind failure logs warning instead of log.Fatal killing the process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-28 13:01:41 -07:00
82 changed files with 995 additions and 1544 deletions
+33 -6
View File
@@ -1,17 +1,44 @@
# MeshCore Analyzer — Environment Configuration
# Copy to .env and customize. All values have sensible defaults in docker-compose.yml.
# Copy to .env and customize. All values have sensible defaults.
#
# This file is read by BOTH docker compose AND manage.sh — one source of truth.
# Each environment keeps config + data together in one directory:
# ~/meshcore-data/config.json, meshcore.db, Caddyfile, theme.json
# ~/meshcore-staging-data/config.json, meshcore.db, Caddyfile
# --- Production ---
PROD_HTTP_PORT=80
PROD_HTTPS_PORT=443
PROD_MQTT_PORT=1883
# Data directory (database, theme, etc.)
# Default: ~/meshcore-data
# Used by: docker compose, manage.sh
PROD_DATA_DIR=~/meshcore-data
# HTTP port for web UI
# Default: 80
# Used by: docker compose
PROD_HTTP_PORT=80
# HTTPS port for web UI (TLS via Caddy)
# Default: 443
# Used by: docker compose
PROD_HTTPS_PORT=443
# MQTT port for observer connections
# Default: 1883
# Used by: docker compose
PROD_MQTT_PORT=1883
# --- Staging (HTTP only, no HTTPS) ---
STAGING_HTTP_PORT=81
STAGING_MQTT_PORT=1884
# Data directory
# Default: ~/meshcore-staging-data
# Used by: docker compose
STAGING_DATA_DIR=~/meshcore-staging-data
# HTTP port
# Default: 81
# Used by: docker compose
STAGING_HTTP_PORT=81
# MQTT port
# Default: 1884
# Used by: docker compose
STAGING_MQTT_PORT=1884
+61
View File
@@ -0,0 +1,61 @@
---
name: "MeshCore PR Reviewer"
description: "A specialized agent for reviewing pull requests in the meshcore-analyzer repository. It focuses on SOLID, DRY, testing, Go best practices, frontend testability, observability, and performance to prevent regressions and maintain high code quality."
model: "gpt-5.3-codex"
tools: ["githubread", "add_issue_comment"]
---
# MeshCore PR Reviewer Agent
You are an expert software engineer specializing in Go and JavaScript-heavy network analysis tools. Your primary role is to act as a meticulous pull request reviewer for the `Kpa-clawbot/meshcore-analyzer` repository. You are deeply familiar with its architecture, as outlined in `AGENTS.md`, and you enforce its rules rigorously.
Your reviews are thorough, constructive, and aimed at maintaining the highest standards of code quality, performance, and stability on both the backend and frontend.
## Core Principles
1. **Context is King**: Before any review, consult the `AGENTS.md` file in the `Kpa-clawbot/meshcore-analyzer` repository to ground your feedback in the project's established architecture and rules.
2. **Enforce the Rules**: Your primary directive is to ensure every rule in `AGENTS.md` is followed. Call out any deviation.
3. **Go & JS Best Practices**: Apply your deep knowledge of Go and modern JavaScript idioms. Pay close attention to concurrency, error handling, performance, and state management, especially as they relate to a real-time data processing application.
4. **Constructive and Educational**: Your feedback should not only identify issues but also explain *why* they are issues and suggest idiomatic solutions. Your goal is to mentor and elevate the codebase and its contributors.
5. **Be a Guardian**: Protect the project from regressions, performance degradation, and architectural drift.
## Review Focus Areas
You will pay special attention to the following areas during your review:
### 1. Architectural Adherence & Design Principles
- **SOLID & DRY**: Does the change adhere to SOLID principles? Is there duplicated logic that could be refactored? Does it respect the existing separation of concerns?
- **Project Architecture**: Does the PR respect the single Node.js server + static frontend architecture? Are changes in the right place?
### 2. Testing and Validation
- **No commit without tests**: Is the backend logic change covered by unit tests? Is `test-packet-filter.js` or `test-aging.js` updated if necessary?
- **Browser Validation**: Has the contributor confirmed the change works in a browser? Is there a screenshot for visual changes?
- **Cache Busters**: If any `public/` assets (`.js`, `.css`) were modified, has the cache buster in `public/index.html` been bumped in the *same commit*? This is critical.
### 3. Go-Specific Concerns
- **Concurrency**: Are goroutines used safely? Are there potential race conditions? Is synchronization used correctly?
- **Error Handling**: Is error handling explicit and clear? Are errors wrapped with context where appropriate?
- **Performance**: Are there inefficient loops or memory allocation patterns? Scrutinize any new data processing logic.
- **Go Idioms**: Does the code follow standard Go idioms and formatting (`gofmt`)?
### 4. Frontend and UI Testability
- **Acknowledge Complexity**: Does the PR introduce complex client-side logic? Recognize that browser-based functionality is difficult to unit test.
- **Promote Testability**: Challenge the contributor to refactor UI code to improve testability. Are data manipulation, state management, and rendering logic separated? Logic should be in pure, testable functions, not tangled in DOM manipulation code.
- **UI Logic Purity**: Scrutinize client-side JavaScript. Are there large, monolithic functions? Could business logic be extracted from event handlers into standalone, easily testable functions?
- **State Management**: How is client-side state managed? Are there risks of race conditions or inconsistent states from asynchronous operations (e.g., API calls)?
### 5. Observability and Maintainability
- **Logging**: Are new logic paths and error cases instrumented with sufficient logging to be debuggable in production?
- **Configuration**: Are new configurable values (thresholds, timeouts) identified for future inclusion in the customizer, as per project rules?
- **Clarity**: Is the code clear, readable, and well-documented where complexity is unavoidable?
### 6. API and Data Integrity
- **API Response Shape**: If the PR adds a UI feature that consumes an API, is there evidence the author verified the actual API response?
- **Firmware as Source of Truth**: For any changes related to the MeshCore protocol, has the author referenced the `firmware/` source? Challenge any "magic numbers" or assumptions about packet structure.
## Review Process
1. **State Your Role**: Begin your review by announcing your function: "As the MeshCore PR Reviewer, I have analyzed this pull request based on the project's architectural guidelines and best practices."
2. **Provide a Summary**: Give a high-level summary of your findings (e.g., "This PR looks solid but needs additions to testing," or "I have several concerns regarding performance and frontend testability.").
3. **Detailed Feedback**: Use a bulleted list to present specific, actionable feedback, referencing file paths and line numbers. For each point, cite the relevant principle or project rule (e.g., "Missing Test Coverage (Rule #1)", "UI Logic Purity (Focus Area #4)").
4. **End with a Clear Approval Status**: Conclude with a clear statement of "Approved" (with minor optional suggestions), "Changes Requested," or "Rejected" (for significant violations).
@@ -0,0 +1,61 @@
---
name: "MeshCore PR Reviewer"
description: "A specialized agent for reviewing pull requests in the meshcore-analyzer repository. It focuses on SOLID, DRY, testing, Go best practices, frontend testability, observability, and performance to prevent regressions and maintain high code quality."
model: "gpt-5.3-codex"
tools: ["githubread", "add_issue_comment"]
---
# MeshCore PR Reviewer Agent
You are an expert software engineer specializing in Go and JavaScript-heavy network analysis tools. Your primary role is to act as a meticulous pull request reviewer for the `Kpa-clawbot/meshcore-analyzer` repository. You are deeply familiar with its architecture, as outlined in `AGENTS.md`, and you enforce its rules rigorously.
Your reviews are thorough, constructive, and aimed at maintaining the highest standards of code quality, performance, and stability on both the backend and frontend.
## Core Principles
1. **Context is King**: Before any review, consult the `AGENTS.md` file in the `Kpa-clawbot/meshcore-analyzer` repository to ground your feedback in the project's established architecture and rules.
2. **Enforce the Rules**: Your primary directive is to ensure every rule in `AGENTS.md` is followed. Call out any deviation.
3. **Go & JS Best Practices**: Apply your deep knowledge of Go and modern JavaScript idioms. Pay close attention to concurrency, error handling, performance, and state management, especially as they relate to a real-time data processing application.
4. **Constructive and Educational**: Your feedback should not only identify issues but also explain *why* they are issues and suggest idiomatic solutions. Your goal is to mentor and elevate the codebase and its contributors.
5. **Be a Guardian**: Protect the project from regressions, performance degradation, and architectural drift.
## Review Focus Areas
You will pay special attention to the following areas during your review:
### 1. Architectural Adherence & Design Principles
- **SOLID & DRY**: Does the change adhere to SOLID principles? Is there duplicated logic that could be refactored? Does it respect the existing separation of concerns?
- **Project Architecture**: Does the PR respect the single Node.js server + static frontend architecture? Are changes in the right place?
### 2. Testing and Validation
- **No commit without tests**: Is the backend logic change covered by unit tests? Is `test-packet-filter.js` or `test-aging.js` updated if necessary?
- **Browser Validation**: Has the contributor confirmed the change works in a browser? Is there a screenshot for visual changes?
- **Cache Busters**: If any `public/` assets (`.js`, `.css`) were modified, has the cache buster in `public/index.html` been bumped in the *same commit*? This is critical.
### 3. Go-Specific Concerns
- **Concurrency**: Are goroutines used safely? Are there potential race conditions? Is synchronization used correctly?
- **Error Handling**: Is error handling explicit and clear? Are errors wrapped with context where appropriate?
- **Performance**: Are there inefficient loops or memory allocation patterns? Scrutinize any new data processing logic.
- **Go Idioms**: Does the code follow standard Go idioms and formatting (`gofmt`)?
### 4. Frontend and UI Testability
- **Acknowledge Complexity**: Does the PR introduce complex client-side logic? Recognize that browser-based functionality is difficult to unit test.
- **Promote Testability**: Challenge the contributor to refactor UI code to improve testability. Are data manipulation, state management, and rendering logic separated? Logic should be in pure, testable functions, not tangled in DOM manipulation code.
- **UI Logic Purity**: Scrutinize client-side JavaScript. Are there large, monolithic functions? Could business logic be extracted from event handlers into standalone, easily testable functions?
- **State Management**: How is client-side state managed? Are there risks of race conditions or inconsistent states from asynchronous operations (e.g., API calls)?
### 5. Observability and Maintainability
- **Logging**: Are new logic paths and error cases instrumented with sufficient logging to be debuggable in production?
- **Configuration**: Are new configurable values (thresholds, timeouts) identified for future inclusion in the customizer, as per project rules?
- **Clarity**: Is the code clear, readable, and well-documented where complexity is unavoidable?
### 6. API and Data Integrity
- **API Response Shape**: If the PR adds a UI feature that consumes an API, is there evidence the author verified the actual API response?
- **Firmware as Source of Truth**: For any changes related to the MeshCore protocol, has the author referenced the `firmware/` source? Challenge any "magic numbers" or assumptions about packet structure.
## Review Process
1. **State Your Role**: Begin your review by announcing your function: "As the MeshCore PR Reviewer, I have analyzed this pull request based on the project's architectural guidelines and best practices."
2. **Provide a Summary**: Give a high-level summary of your findings (e.g., "This PR looks solid but needs additions to testing," or "I have several concerns regarding performance and frontend testability.").
3. **Detailed Feedback**: Use a bulleted list to present specific, actionable feedback, referencing file paths and line numbers. For each point, cite the relevant principle or project rule (e.g., "Missing Test Coverage (Rule #1)", "UI Logic Purity (Focus Area #4)").
4. **End with a Clear Approval Status**: Conclude with a clear statement of "Approved" (with minor optional suggestions), "Changes Requested," or "Rejected" (for significant violations).
+14 -4
View File
@@ -8,6 +8,13 @@ on:
- 'LICENSE'
- '.gitignore'
- 'docs/**'
pull_request:
branches: [master]
paths-ignore:
- '**.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
concurrency:
group: deploy
@@ -270,6 +277,7 @@ jobs:
# ───────────────────────────────────────────────────────────────
build:
name: "🏗️ Build Docker Image"
if: github.event_name == 'push'
needs: [go-test]
runs-on: self-hosted
steps:
@@ -294,6 +302,7 @@ jobs:
# ───────────────────────────────────────────────────────────────
deploy:
name: "🚀 Deploy Staging"
if: github.event_name == 'push'
needs: [build]
runs-on: self-hosted
steps:
@@ -303,7 +312,7 @@ jobs:
- name: Start staging on port 82
run: |
# Force remove stale containers
docker rm -f meshcore-staging-go 2>/dev/null || true
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
@@ -311,14 +320,14 @@ jobs:
- name: Healthcheck staging container
run: |
for i in $(seq 1 120); do
HEALTH=$(docker inspect meshcore-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
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 meshcore-staging-go --tail 50
docker logs corescope-staging-go --tail 50
exit 1
fi
sleep 1
@@ -338,6 +347,7 @@ jobs:
# ───────────────────────────────────────────────────────────────
publish:
name: "📝 Publish Badges & Summary"
if: github.event_name == 'push'
needs: [deploy]
runs-on: self-hosted
steps:
@@ -378,6 +388,6 @@ jobs:
echo "To promote to production:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "ssh deploy@\$VM_HOST" >> $GITHUB_STEP_SUMMARY
echo "cd /opt/meshcore-deploy" >> $GITHUB_STEP_SUMMARY
echo "cd /opt/corescope-deploy" >> $GITHUB_STEP_SUMMARY
echo "./manage.sh promote" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
+2 -2
View File
@@ -1,10 +1,10 @@
# Bishop — Tester
Unit tests, Playwright E2E, coverage gates, and quality assurance for MeshCore Analyzer.
Unit tests, Playwright E2E, coverage gates, and quality assurance for CoreScope.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Node.js native test runner, Playwright, c8 + nyc (coverage), supertest
**User:** User
+1 -1
View File
@@ -2,7 +2,7 @@
## Project Context
MeshCore Analyzer has 14 test files, 4,290 lines of test code. Backend coverage 85%+, frontend 42%+. Tests use Node.js native runner, Playwright for E2E, c8/nyc for coverage, supertest for API routes. vm.createContext pattern used for testing frontend helpers in Node.js.
CoreScope has 14 test files, 4,290 lines of test code. Backend coverage 85%+, frontend 42%+. Tests use Node.js native runner, Playwright for E2E, c8/nyc for coverage, supertest for API routes. vm.createContext pattern used for testing frontend helpers in Node.js.
User: User
+2 -2
View File
@@ -1,10 +1,10 @@
# Hicks — Backend Dev
Server, decoder, packet-store, SQLite, API, MQTT, WebSocket, and performance for MeshCore Analyzer.
Server, decoder, packet-store, SQLite, API, MQTT, WebSocket, and performance for CoreScope.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Node.js 18+, Express 5, SQLite (better-sqlite3), MQTT (mqtt), WebSocket (ws)
**User:** User
+1 -1
View File
@@ -2,7 +2,7 @@
## Project Context
MeshCore Analyzer is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend. Custom decoder.js fixes path_length bug from upstream library. In-memory packet store provides O(1) lookups for 30K+ packets. TTL response cache achieves 7,000× speedup on bulk health endpoint.
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend. Custom decoder.js fixes path_length bug from upstream library. In-memory packet store provides O(1) lookups for 30K+ packets. TTL response cache achieves 7,000× speedup on bulk health endpoint.
User: User
+2 -2
View File
@@ -1,10 +1,10 @@
# Kobayashi — Lead
Architecture, code review, and decision-making for MeshCore Analyzer.
Architecture, code review, and decision-making for CoreScope.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Node.js 18+, Express 5, SQLite, vanilla JS frontend, Leaflet, WebSocket, MQTT
**User:** User
+1 -1
View File
@@ -2,7 +2,7 @@
## Project Context
MeshCore Analyzer is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend with Leaflet maps, WebSocket live feed, MQTT ingestion. Production at v2.6.0, ~18K lines, 85%+ backend test coverage.
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend with Leaflet maps, WebSocket live feed, MQTT ingestion. Production at v2.6.0, ~18K lines, 85%+ backend test coverage.
User: User
+2 -2
View File
@@ -1,10 +1,10 @@
# Newt — Frontend Dev
Vanilla JS UI, Leaflet maps, live visualization, theming, and all public/ modules for MeshCore Analyzer.
Vanilla JS UI, Leaflet maps, live visualization, theming, and all public/ modules for CoreScope.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Vanilla HTML/CSS/JavaScript (ES5/6), Leaflet maps, WebSocket, Canvas animations
**User:** User
+1 -1
View File
@@ -2,7 +2,7 @@
## Project Context
MeshCore Analyzer is a real-time LoRa mesh packet analyzer with a vanilla JS SPA frontend. 22 frontend modules, Leaflet maps, WebSocket live feed, VCR playback, Canvas animations, theme customizer with CSS variables. No build step, no framework. ES5/6 for broad browser support.
CoreScope is a real-time LoRa mesh packet analyzer with a vanilla JS SPA frontend. 22 frontend modules, Leaflet maps, WebSocket live feed, VCR playback, Canvas animations, theme customizer with CSS variables. No build step, no framework. ES5/6 for broad browser support.
User: User
+1 -1
View File
@@ -4,7 +4,7 @@ Tracks the work queue and keeps the team moving. Always on the roster.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**User:** User
## Responsibilities
+2 -2
View File
@@ -1,10 +1,10 @@
# Ripley — Support Engineer
Deep knowledge of every frontend behavior, API response, and user-facing feature in MeshCore Analyzer. Fields community questions, triages bug reports, and explains "why does X look like Y."
Deep knowledge of every frontend behavior, API response, and user-facing feature in CoreScope. Fields community questions, triages bug reports, and explains "why does X look like Y."
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Vanilla JS frontend (public/*.js), Node.js backend, SQLite, WebSocket, MQTT
**User:** Kpa-clawbot
+1 -1
View File
@@ -1,7 +1,7 @@
# Ripley — Support Engineer History
## Core Context
- Project: MeshCore Analyzer — real-time LoRa mesh packet analyzer
- Project: CoreScope — real-time LoRa mesh packet analyzer
- User: Kpa-clawbot
- Joined the team 2026-03-27 to handle community support and triage
+2 -2
View File
@@ -1,10 +1,10 @@
# Scribe — Session Logger
Silent agent that maintains decisions, logs, and cross-agent context for MeshCore Analyzer.
Silent agent that maintains decisions, logs, and cross-agent context for CoreScope.
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**User:** User
## Responsibilities
+1 -1
View File
@@ -5,7 +5,7 @@
"universe": "aliens",
"created_at": "2026-03-26T04:22:08Z",
"agents": ["Kobayashi", "Hicks", "Newt", "Bishop"],
"reason": "Initial team casting for MeshCore Analyzer project"
"reason": "Initial team casting for CoreScope project"
}
]
}
+2 -2
View File
@@ -1,8 +1,8 @@
# Squad — MeshCore Analyzer
# Squad — CoreScope
## Project Context
**Project:** MeshCore Analyzer — Real-time LoRa mesh packet analyzer
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
**Stack:** Node.js 18+, Express 5, SQLite (better-sqlite3), vanilla JS frontend, Leaflet maps, WebSocket (ws), MQTT (mqtt)
**User:** User
**Description:** Self-hosted alternative to analyzer.letsmesh.net. Ingests MeshCore mesh network packets via MQTT, decodes with custom parser (decoder.js), stores in SQLite with in-memory indexing (packet-store.js), and serves a rich SPA with live visualization, packet analysis, node analytics, channel chat, observer health, and theme customizer. ~18K lines, 14 test files, 85%+ backend coverage. Production at v2.6.0.
+1 -1
View File
@@ -1,4 +1,4 @@
# AGENTS.md — MeshCore Analyzer
# AGENTS.md — CoreScope
Guide for AI agents working on this codebase. Read this before writing any code.
+6 -6
View File
@@ -11,14 +11,14 @@ WORKDIR /build/server
COPY cmd/server/go.mod cmd/server/go.sum ./
RUN go mod download
COPY cmd/server/ ./
RUN go build -ldflags "-X main.Version=${APP_VERSION} -X main.Commit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" -o /meshcore-server .
RUN go build -ldflags "-X main.Version=${APP_VERSION} -X main.Commit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" -o /corescope-server .
# Build ingestor
WORKDIR /build/ingestor
COPY cmd/ingestor/go.mod cmd/ingestor/go.sum ./
RUN go mod download
COPY cmd/ingestor/ ./
RUN go build -o /meshcore-ingestor .
RUN go build -o /corescope-ingestor .
# Runtime image
FROM alpine:3.20
@@ -28,15 +28,15 @@ RUN apk add --no-cache mosquitto mosquitto-clients supervisor caddy wget
WORKDIR /app
# Go binaries
COPY --from=builder /meshcore-server /meshcore-ingestor /app/
COPY --from=builder /corescope-server /corescope-ingestor /app/
# Frontend assets + config
COPY public/ ./public/
COPY config.example.json channel-rainbow.json ./
# Bake git commit SHA (CI writes .git-commit before build; fallback for non-ldflags usage)
COPY .git-commi[t] ./
RUN if [ ! -f .git-commit ]; then echo "unknown" > .git-commit; fi
# Bake git commit SHA — manage.sh and CI write .git-commit before build
# Default to "unknown" if not provided
RUN echo "unknown" > .git-commit
# Supervisor + Mosquitto + Caddy config
COPY docker/supervisord-go.conf /etc/supervisor/conf.d/supervisord.conf
+3 -3
View File
@@ -11,14 +11,14 @@ WORKDIR /build/server
COPY cmd/server/go.mod cmd/server/go.sum ./
RUN go mod download
COPY cmd/server/ ./
RUN go build -ldflags "-X main.Version=${APP_VERSION} -X main.Commit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" -o /meshcore-server .
RUN go build -ldflags "-X main.Version=${APP_VERSION} -X main.Commit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" -o /corescope-server .
# Build ingestor
WORKDIR /build/ingestor
COPY cmd/ingestor/go.mod cmd/ingestor/go.sum ./
RUN go mod download
COPY cmd/ingestor/ ./
RUN go build -o /meshcore-ingestor .
RUN go build -o /corescope-ingestor .
# Runtime image
FROM alpine:3.20
@@ -28,7 +28,7 @@ RUN apk add --no-cache mosquitto mosquitto-clients supervisor caddy wget
WORKDIR /app
# Go binaries
COPY --from=builder /meshcore-server /meshcore-ingestor /app/
COPY --from=builder /corescope-server /corescope-ingestor /app/
# Frontend assets + config
COPY public/ ./public/
+9 -9
View File
@@ -1,10 +1,10 @@
# MeshCore Analyzer
# CoreScope
[![Go Server Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/.badges/go-server-coverage.json)](https://github.com/Kpa-clawbot/meshcore-analyzer/actions/workflows/deploy.yml)
[![Go Ingestor Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/.badges/go-ingestor-coverage.json)](https://github.com/Kpa-clawbot/meshcore-analyzer/actions/workflows/deploy.yml)
[![Frontend Tests](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/.badges/frontend-tests.json)](https://github.com/Kpa-clawbot/meshcore-analyzer/actions/workflows/deploy.yml)
[![Frontend Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/.badges/frontend-coverage.json)](https://github.com/Kpa-clawbot/meshcore-analyzer/actions/workflows/deploy.yml)
[![Deploy](https://github.com/Kpa-clawbot/meshcore-analyzer/actions/workflows/deploy.yml/badge.svg)](https://github.com/Kpa-clawbot/meshcore-analyzer/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,8 +79,8 @@ 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/meshcore-analyzer.git
cd meshcore-analyzer
git clone https://github.com/Kpa-clawbot/corescope.git
cd corescope
./manage.sh setup
```
@@ -171,7 +171,7 @@ Or POST raw hex packets to `POST /api/packets` for manual injection.
## Project Structure
```
meshcore-analyzer/
corescope/
├── cmd/
│ ├── server/ # Go HTTP server + WebSocket + REST API
│ │ ├── main.go # Entry point
+4 -4
View File
@@ -73,8 +73,8 @@ Advert counts now reflect unique transmissions, not total observations. A packet
The Go backend is two binaries managed by supervisord inside Docker:
- **`meshcore-ingestor`** — connects to MQTT brokers, decodes packets, writes to SQLite, maintains the in-memory store
- **`meshcore-server`** — HTTP API, WebSocket broadcast, static file serving, analytics computation
- **`corescope-ingestor`** — connects to MQTT brokers, decodes packets, writes to SQLite, maintains the in-memory store
- **`corescope-server`** — HTTP API, WebSocket broadcast, static file serving, analytics computation
Both share the same SQLite database (WAL mode). The frontend is unchanged — same vanilla JS, same `public/` directory, served by the Go HTTP server through Caddy.
@@ -120,7 +120,7 @@ curl -s http://localhost/api/health | grep engine
The Node.js Dockerfile is preserved as `Dockerfile.node`:
```bash
docker build -f Dockerfile.node -t meshcore-analyzer:latest .
docker build -f Dockerfile.node -t corescope:latest .
docker compose up -d --force-recreate prod
```
@@ -152,7 +152,7 @@ This release wouldn't exist without the community:
- **LitBomb** — issue reports from production deployments
- **mibzzer15** — issue reports and edge case discovery
And to everyone running MeshCore Analyzer in the wild — your packet data, bug reports, and feature requests are what drive this project forward. The Go rewrite happened because the community outgrew what Node.js could handle. 56K packets, dozens of observers, sub-second queries. This is your tool. We just rewrote the engine.
And to everyone running CoreScope in the wild — your packet data, bug reports, and feature requests are what drive this project forward. The Go rewrite happened because the community outgrew what Node.js could handle. 56K packets, dozens of observers, sub-second queries. This is your tool. We just rewrote the engine.
---
+1 -1
View File
@@ -148,7 +148,7 @@ async function benchmarkEndpoints(port, endpoints, nocache = false) {
}
async function run() {
console.log(`\nMeshCore Analyzer Benchmark — ${RUNS} runs per endpoint`);
console.log(`\nCoreScope Benchmark — ${RUNS} runs per endpoint`);
console.log('Launching servers...\n');
// Launch both servers
+4 -4
View File
@@ -1,6 +1,6 @@
# MeshCore MQTT Ingestor (Go)
Standalone MQTT ingestion service for MeshCore Analyzer. Connects to MQTT brokers, decodes raw MeshCore packets, and writes to the same SQLite database used by the Node.js web server.
Standalone MQTT ingestion service for CoreScope. Connects to MQTT brokers, decodes raw MeshCore packets, and writes to the same SQLite database used by the Node.js web server.
This is the first step of a larger Go rewrite — separating MQTT ingestion from the web server.
@@ -23,19 +23,19 @@ Requires Go 1.22+.
```bash
cd cmd/ingestor
go build -o meshcore-ingestor .
go build -o corescope-ingestor .
```
Cross-compile for Linux (e.g., for the production VM):
```bash
GOOS=linux GOARCH=amd64 go build -o meshcore-ingestor .
GOOS=linux GOARCH=amd64 go build -o corescope-ingestor .
```
## Run
```bash
./meshcore-ingestor -config /path/to/config.json
./corescope-ingestor -config /path/to/config.json
```
The config file uses the same format as the Node.js `config.json`. The ingestor reads the `mqttSources` array (or legacy `mqtt` object) and `dbPath` fields.
+8 -7
View File
@@ -26,13 +26,14 @@ type MQTTLegacy struct {
// Config holds the ingestor configuration, compatible with the Node.js config.json format.
type Config struct {
DBPath string `json:"dbPath"`
MQTT *MQTTLegacy `json:"mqtt,omitempty"`
MQTTSources []MQTTSource `json:"mqttSources,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
ChannelKeysPath string `json:"channelKeysPath,omitempty"`
ChannelKeys map[string]string `json:"channelKeys,omitempty"`
Retention *RetentionConfig `json:"retention,omitempty"`
DBPath string `json:"dbPath"`
MQTT *MQTTLegacy `json:"mqtt,omitempty"`
MQTTSources []MQTTSource `json:"mqttSources,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
ChannelKeysPath string `json:"channelKeysPath,omitempty"`
ChannelKeys map[string]string `json:"channelKeys,omitempty"`
HashChannels []string `json:"hashChannels,omitempty"`
Retention *RetentionConfig `json:"retention,omitempty"`
}
// RetentionConfig controls how long stale nodes are kept before being moved to inactive_nodes.
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/meshcore-analyzer/ingestor
module github.com/corescope/ingestor
go 1.22
+39 -7
View File
@@ -30,7 +30,9 @@ func main() {
}
go func() {
log.Printf("[pprof] ingestor profiling at http://localhost:%s/debug/pprof/", pprofPort)
log.Fatal(http.ListenAndServe(":"+pprofPort, nil))
if err := http.ListenAndServe(":"+pprofPort, nil); err != nil {
log.Printf("[pprof] failed to start: %v (non-fatal)", err)
}
}()
}
@@ -510,34 +512,64 @@ func firstNonEmpty(vals ...string) string {
return ""
}
// deriveHashtagChannelKey derives an AES-128 key from a channel name.
// Same algorithm as Node.js: SHA-256(channelName) → first 32 hex chars (16 bytes).
func deriveHashtagChannelKey(channelName string) string {
h := sha256.Sum256([]byte(channelName))
return hex.EncodeToString(h[:16])
}
// loadChannelKeys loads channel decryption keys from config and/or a JSON file.
// Priority: CHANNEL_KEYS_PATH env var > cfg.ChannelKeysPath > channel-rainbow.json next to config.
// Merge priority: rainbow (lowest) → derived from hashChannels → explicit config (highest).
func loadChannelKeys(cfg *Config, configPath string) map[string]string {
keys := make(map[string]string)
// Determine file path for rainbow keys
// 1. Rainbow table keys (lowest priority)
keysPath := os.Getenv("CHANNEL_KEYS_PATH")
if keysPath == "" {
keysPath = cfg.ChannelKeysPath
}
if keysPath == "" {
// Default: look for channel-rainbow.json next to config file
keysPath = filepath.Join(filepath.Dir(configPath), "channel-rainbow.json")
}
rainbowCount := 0
if data, err := os.ReadFile(keysPath); err == nil {
var fileKeys map[string]string
if err := json.Unmarshal(data, &fileKeys); err == nil {
for k, v := range fileKeys {
keys[k] = v
}
log.Printf("Loaded %d channel keys from %s", len(fileKeys), keysPath)
rainbowCount = len(fileKeys)
log.Printf("Loaded %d channel keys from %s", rainbowCount, keysPath)
} else {
log.Printf("Warning: failed to parse channel keys file %s: %v", keysPath, err)
}
}
// Merge inline config keys (override file keys)
// 2. Derived keys from hashChannels (middle priority)
derivedCount := 0
for _, raw := range cfg.HashChannels {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
continue
}
channelName := trimmed
if !strings.HasPrefix(channelName, "#") {
channelName = "#" + channelName
}
// Skip if explicit config already has this key
if _, exists := cfg.ChannelKeys[channelName]; exists {
continue
}
keys[channelName] = deriveHashtagChannelKey(channelName)
derivedCount++
}
if derivedCount > 0 {
log.Printf("[channels] %d derived from hashChannels", derivedCount)
}
// 3. Explicit config keys (highest priority — overrides rainbow + derived)
for k, v := range cfg.ChannelKeys {
keys[k] = v
}
@@ -550,7 +582,7 @@ var version = "dev"
func init() {
if len(os.Args) > 1 && os.Args[1] == "--version" {
fmt.Println("meshcore-ingestor", version)
fmt.Println("corescope-ingestor", version)
os.Exit(0)
}
}
+131
View File
@@ -3,6 +3,8 @@ package main
import (
"encoding/json"
"math"
"os"
"path/filepath"
"testing"
"time"
)
@@ -492,3 +494,132 @@ func TestAdvertRole(t *testing.T) {
})
}
}
func TestDeriveHashtagChannelKey(t *testing.T) {
// Test vectors validated against Node.js server-helpers.js
tests := []struct {
name string
want string
}{
{"#General", "649af2cab73ed5a890890a5485a0c004"},
{"#test", "9cd8fcf22a47333b591d96a2b848b73f"},
{"#MeshCore", "dcf73f393fa217f6b28fcec6ffc411ad"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := deriveHashtagChannelKey(tt.name)
if got != tt.want {
t.Errorf("deriveHashtagChannelKey(%q) = %q, want %q", tt.name, got, tt.want)
}
})
}
// Deterministic
k1 := deriveHashtagChannelKey("#foo")
k2 := deriveHashtagChannelKey("#foo")
if k1 != k2 {
t.Error("deriveHashtagChannelKey should be deterministic")
}
// Returns 32-char hex string (16 bytes)
if len(k1) != 32 {
t.Errorf("key length = %d, want 32", len(k1))
}
// Different inputs → different keys
k3 := deriveHashtagChannelKey("#bar")
if k1 == k3 {
t.Error("different inputs should produce different keys")
}
}
func TestLoadChannelKeysMergePriority(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json")
// Create a rainbow file with two keys: #rainbow (unique) and #override (to be overridden)
rainbowPath := filepath.Join(dir, "channel-rainbow.json")
t.Setenv("CHANNEL_KEYS_PATH", rainbowPath)
rainbow := map[string]string{
"#rainbow": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"#override": "rainbow_value_should_be_overridden",
}
rainbowJSON, err := json.Marshal(rainbow)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(rainbowPath, rainbowJSON, 0o644); err != nil {
t.Fatal(err)
}
cfg := &Config{
HashChannels: []string{"General", "#override"},
ChannelKeys: map[string]string{"#override": "explicit_wins"},
}
keys := loadChannelKeys(cfg, cfgPath)
// Rainbow key loaded
if keys["#rainbow"] != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
t.Errorf("rainbow key missing or wrong: %q", keys["#rainbow"])
}
// HashChannels derived #General
expected := deriveHashtagChannelKey("#General")
if keys["#General"] != expected {
t.Errorf("#General = %q, want %q (derived)", keys["#General"], expected)
}
// Explicit config wins over both rainbow and derived
if keys["#override"] != "explicit_wins" {
t.Errorf("#override = %q, want explicit_wins", keys["#override"])
}
}
func TestLoadChannelKeysHashChannelsNormalization(t *testing.T) {
t.Setenv("CHANNEL_KEYS_PATH", "")
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json")
cfg := &Config{
HashChannels: []string{
"NoPound", // should become #NoPound
"#HasPound", // stays #HasPound
" Spaced ", // trimmed → #Spaced
"", // skipped
},
}
keys := loadChannelKeys(cfg, cfgPath)
if _, ok := keys["#NoPound"]; !ok {
t.Error("should derive key for #NoPound (auto-prefixed)")
}
if _, ok := keys["#HasPound"]; !ok {
t.Error("should derive key for #HasPound")
}
if _, ok := keys["#Spaced"]; !ok {
t.Error("should derive key for #Spaced (trimmed)")
}
if len(keys) != 3 {
t.Errorf("expected 3 keys, got %d", len(keys))
}
}
func TestLoadChannelKeysSkipExplicit(t *testing.T) {
t.Setenv("CHANNEL_KEYS_PATH", "")
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json")
cfg := &Config{
HashChannels: []string{"General"},
ChannelKeys: map[string]string{"#General": "my_explicit_key"},
}
keys := loadChannelKeys(cfg, cfgPath)
// Explicit key should win — hashChannels derivation should be skipped
if keys["#General"] != "my_explicit_key" {
t.Errorf("#General = %q, want my_explicit_key", keys["#General"])
}
}
+6 -28
View File
@@ -46,14 +46,6 @@ func setupTestDBv2(t *testing.T) *DB {
observer_id TEXT, observer_name TEXT, direction TEXT,
snr REAL, rssi REAL, score INTEGER, path_json TEXT, timestamp INTEGER NOT NULL
);
CREATE VIEW packets_v AS
SELECT o.id, t.raw_hex,
strftime('%Y-%m-%dT%H:%M:%fZ', o.timestamp, 'unixepoch') AS timestamp,
o.observer_id, o.observer_name,
o.direction, o.snr, o.rssi, o.score, t.hash, t.route_type,
t.payload_type, t.payload_version, o.path_json, t.decoded_json, t.created_at
FROM observations o
JOIN transmissions t ON t.id = o.transmission_id;
`
if _, err := conn.Exec(schema); err != nil {
t.Fatal(err)
@@ -551,8 +543,8 @@ func TestHandlePacketDetailNoStore(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
if w.Code != 404 {
t.Fatalf("expected 404 (no store), got %d: %s", w.Code, w.Body.String())
}
})
@@ -560,8 +552,8 @@ func TestHandlePacketDetailNoStore(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
if w.Code != 404 {
t.Fatalf("expected 404 (no store), got %d: %s", w.Code, w.Body.String())
}
})
@@ -1475,8 +1467,8 @@ func TestHandleObserverAnalyticsNoStore(t *testing.T) {
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
if w.Code != 503 {
t.Fatalf("expected 503, got %d: %s", w.Code, w.Body.String())
}
}
@@ -3272,20 +3264,6 @@ func TestHandlePacketDetailWithStoreAllPaths(t *testing.T) {
// --- Additional DB function coverage ---
func TestDBGetTimestamps(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
ts, err := db.GetTimestamps("2000-01-01")
if err != nil {
t.Fatal(err)
}
if len(ts) < 1 {
t.Error("expected >=1 timestamps")
}
}
func TestDBGetNewTransmissionsSince(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
+32 -189
View File
@@ -120,14 +120,14 @@ func (db *DB) scanTransmissionRow(rows *sql.Rows) map[string]interface{} {
// Node represents a row from the nodes table.
type Node struct {
PublicKey string `json:"public_key"`
Name *string `json:"name"`
Role *string `json:"role"`
Lat *float64 `json:"lat"`
Lon *float64 `json:"lon"`
LastSeen *string `json:"last_seen"`
FirstSeen *string `json:"first_seen"`
AdvertCount int `json:"advert_count"`
PublicKey string `json:"public_key"`
Name *string `json:"name"`
Role *string `json:"role"`
Lat *float64 `json:"lat"`
Lon *float64 `json:"lon"`
LastSeen *string `json:"last_seen"`
FirstSeen *string `json:"first_seen"`
AdvertCount int `json:"advert_count"`
BatteryMv *int `json:"battery_mv"`
TemperatureC *float64 `json:"temperature_c"`
}
@@ -162,7 +162,7 @@ type Transmission struct {
CreatedAt *string `json:"created_at"`
}
// Observation (from packets_v view).
// Observation (observation-level data).
type Observation struct {
ID int `json:"id"`
RawHex *string `json:"raw_hex"`
@@ -435,7 +435,7 @@ func (db *DB) QueryGroupedPackets(q PacketQuery) (*PacketResult, error) {
w = "WHERE " + strings.Join(where, " AND ")
}
// Count total transmissions (fast — queries transmissions directly, not packets_v)
// Count total transmissions (fast — queries transmissions directly, not a VIEW)
var total int
if len(where) == 0 {
db.conn.QueryRow("SELECT COUNT(*) FROM transmissions").Scan(&total)
@@ -628,18 +628,6 @@ func (db *DB) resolveNodePubkey(nodeIDOrName string) string {
return pk
}
// GetPacketByID fetches a single packet/observation.
func (db *DB) GetPacketByID(id int) (map[string]interface{}, error) {
rows, err := db.conn.Query("SELECT id, raw_hex, timestamp, observer_id, observer_name, direction, snr, rssi, score, hash, route_type, payload_type, payload_version, path_json, decoded_json, created_at FROM packets_v WHERE id = ?", id)
if err != nil {
return nil, err
}
defer rows.Close()
if rows.Next() {
return scanPacketRow(rows), nil
}
return nil, nil
}
// GetTransmissionByID fetches from transmissions table with observer data.
func (db *DB) GetTransmissionByID(id int) (map[string]interface{}, error) {
@@ -673,24 +661,6 @@ func (db *DB) GetPacketByHash(hash string) (map[string]interface{}, error) {
return nil, nil
}
// GetObservationsForHash returns all observations for a given hash.
func (db *DB) GetObservationsForHash(hash string) ([]map[string]interface{}, error) {
rows, err := db.conn.Query(`SELECT id, raw_hex, timestamp, observer_id, observer_name, direction, snr, rssi, score, hash, route_type, payload_type, payload_version, path_json, decoded_json, created_at
FROM packets_v WHERE hash = ? ORDER BY timestamp DESC`, strings.ToLower(hash))
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]map[string]interface{}, 0)
for rows.Next() {
p := scanPacketRow(rows)
if p != nil {
result = append(result, p)
}
}
return result, nil
}
// GetNodes returns filtered, paginated node list.
func (db *DB) GetNodes(limit, offset int, role, search, before, lastHeard, sortBy, region string) ([]map[string]interface{}, int, map[string]int, error) {
@@ -798,30 +768,6 @@ func (db *DB) GetNodeByPubkey(pubkey string) (map[string]interface{}, error) {
return nil, nil
}
// GetRecentPacketsForNode returns recent packets referencing a node.
func (db *DB) GetRecentPacketsForNode(pubkey string, name string, limit int) ([]map[string]interface{}, error) {
if limit <= 0 {
limit = 20
}
pk := "%" + pubkey + "%"
np := "%" + name + "%"
rows, err := db.conn.Query(`SELECT id, raw_hex, timestamp, observer_id, observer_name, direction, snr, rssi, score, hash, route_type, payload_type, payload_version, path_json, decoded_json, created_at
FROM packets_v WHERE decoded_json LIKE ? OR decoded_json LIKE ?
ORDER BY timestamp DESC LIMIT ?`, pk, np, limit)
if err != nil {
return nil, err
}
defer rows.Close()
packets := make([]map[string]interface{}, 0)
for rows.Next() {
p := scanPacketRow(rows)
if p != nil {
packets = append(packets, p)
}
}
return packets, nil
}
// GetRecentTransmissionsForNode returns recent transmissions referencing a node (Node.js-compatible shape).
func (db *DB) GetRecentTransmissionsForNode(pubkey string, name string, limit int) ([]map[string]interface{}, error) {
@@ -1045,103 +991,6 @@ func (db *DB) GetDistinctIATAs() ([]string, error) {
return codes, nil
}
// GetNodeHealth returns health info for a node (observers, stats, recent packets).
func (db *DB) GetNodeHealth(pubkey string) (map[string]interface{}, error) {
node, err := db.GetNodeByPubkey(pubkey)
if err != nil || node == nil {
return nil, err
}
name := ""
if n, ok := node["name"]; ok && n != nil {
name = fmt.Sprintf("%v", n)
}
pk := "%" + pubkey + "%"
np := "%" + name + "%"
whereClause := "decoded_json LIKE ? OR decoded_json LIKE ?"
if name == "" {
whereClause = "decoded_json LIKE ?"
np = pk
}
todayStart := time.Now().UTC().Truncate(24 * time.Hour).Format(time.RFC3339)
// Observers
observerSQL := fmt.Sprintf(`SELECT observer_id, observer_name, AVG(snr) as avgSnr, AVG(rssi) as avgRssi, COUNT(*) as packetCount
FROM packets_v WHERE (%s) AND observer_id IS NOT NULL GROUP BY observer_id ORDER BY packetCount DESC`, whereClause)
oRows, err := db.conn.Query(observerSQL, pk, np)
if err != nil {
return nil, err
}
defer oRows.Close()
observers := make([]map[string]interface{}, 0)
for oRows.Next() {
var obsID, obsName sql.NullString
var avgSnr, avgRssi sql.NullFloat64
var pktCount int
oRows.Scan(&obsID, &obsName, &avgSnr, &avgRssi, &pktCount)
observers = append(observers, map[string]interface{}{
"observer_id": nullStr(obsID),
"observer_name": nullStr(obsName),
"avgSnr": nullFloat(avgSnr),
"avgRssi": nullFloat(avgRssi),
"packetCount": pktCount,
})
}
// Stats
var packetsToday, totalPackets int
db.conn.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM packets_v WHERE (%s) AND timestamp > ?", whereClause), pk, np, todayStart).Scan(&packetsToday)
db.conn.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM packets_v WHERE (%s)", whereClause), pk, np).Scan(&totalPackets)
var avgSnr sql.NullFloat64
db.conn.QueryRow(fmt.Sprintf("SELECT AVG(snr) FROM packets_v WHERE (%s)", whereClause), pk, np).Scan(&avgSnr)
var lastHeard sql.NullString
db.conn.QueryRow(fmt.Sprintf("SELECT MAX(timestamp) FROM packets_v WHERE (%s)", whereClause), pk, np).Scan(&lastHeard)
// Avg hops
hRows, _ := db.conn.Query(fmt.Sprintf("SELECT path_json FROM packets_v WHERE (%s) AND path_json IS NOT NULL", whereClause), pk, np)
totalHops, hopCount := 0, 0
if hRows != nil {
defer hRows.Close()
for hRows.Next() {
var pj sql.NullString
hRows.Scan(&pj)
if pj.Valid {
var hops []interface{}
if json.Unmarshal([]byte(pj.String), &hops) == nil {
totalHops += len(hops)
hopCount++
}
}
}
}
avgHops := 0
if hopCount > 0 {
avgHops = int(math.Round(float64(totalHops) / float64(hopCount)))
}
// Recent packets
recentPackets, _ := db.GetRecentTransmissionsForNode(pubkey, name, 20)
return map[string]interface{}{
"node": node,
"observers": observers,
"stats": map[string]interface{}{
"totalTransmissions": totalPackets,
"totalObservations": totalPackets,
"totalPackets": totalPackets,
"packetsToday": packetsToday,
"avgSnr": nullFloat(avgSnr),
"avgHops": avgHops,
"lastHeard": nullStr(lastHeard),
},
"recentPackets": recentPackets,
}, nil
}
// GetNetworkStatus returns overall network health status.
func (db *DB) GetNetworkStatus(healthThresholds HealthThresholds) (map[string]interface{}, error) {
@@ -1190,10 +1039,28 @@ func (db *DB) GetNetworkStatus(healthThresholds HealthThresholds) (map[string]in
}, nil
}
// GetTraces returns observations for a hash.
// GetTraces returns observations for a hash using direct table queries.
func (db *DB) GetTraces(hash string) ([]map[string]interface{}, error) {
rows, err := db.conn.Query(`SELECT observer_id, observer_name, timestamp, snr, rssi, path_json
FROM packets_v WHERE hash = ? ORDER BY timestamp ASC`, strings.ToLower(hash))
var querySQL string
if db.isV3 {
querySQL = `SELECT obs.id AS observer_id, obs.name AS observer_name,
strftime('%Y-%m-%dT%H:%M:%fZ', o.timestamp, 'unixepoch') AS timestamp,
o.snr, o.rssi, o.path_json
FROM observations o
JOIN transmissions t ON t.id = o.transmission_id
LEFT JOIN observers obs ON obs.rowid = o.observer_idx
WHERE t.hash = ?
ORDER BY o.timestamp ASC`
} else {
querySQL = `SELECT o.observer_id, o.observer_name,
strftime('%Y-%m-%dT%H:%M:%fZ', o.timestamp, 'unixepoch') AS timestamp,
o.snr, o.rssi, o.path_json
FROM observations o
JOIN transmissions t ON t.id = o.transmission_id
WHERE t.hash = ?
ORDER BY o.timestamp ASC`
}
rows, err := db.conn.Query(querySQL, strings.ToLower(hash))
if err != nil {
return nil, err
}
@@ -1219,7 +1086,7 @@ func (db *DB) GetTraces(hash string) ([]map[string]interface{}, error) {
}
// GetChannels returns channel list from GRP_TXT packets.
// Queries transmissions directly (not packets_v) to avoid observation-level
// Queries transmissions directly (not a VIEW) to avoid observation-level
// duplicates that could cause stale lastMessage when an older message has
// a later re-observation timestamp.
func (db *DB) GetChannels() ([]map[string]interface{}, error) {
@@ -1435,31 +1302,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int) ([]map[s
return messages, total, nil
}
// GetTimestamps returns packet timestamps since a given time.
func (db *DB) GetTimestamps(since string) ([]string, error) {
rows, err := db.conn.Query("SELECT timestamp FROM packets_v WHERE timestamp > ? ORDER BY timestamp ASC", since)
if err != nil {
return nil, err
}
defer rows.Close()
var timestamps []string
for rows.Next() {
var ts string
rows.Scan(&ts)
timestamps = append(timestamps, ts)
}
if timestamps == nil {
timestamps = []string{}
}
return timestamps, nil
}
// GetNodeCountsForPacket returns observation count for a hash.
func (db *DB) GetObservationCount(hash string) int {
var count int
db.conn.QueryRow("SELECT COUNT(*) FROM packets_v WHERE hash = ?", strings.ToLower(hash)).Scan(&count)
return count
}
// GetNewTransmissionsSince returns new transmissions after a given ID for WebSocket polling.
func (db *DB) GetNewTransmissionsSince(lastID int, limit int) ([]map[string]interface{}, error) {
+4 -202
View File
@@ -73,16 +73,6 @@ func setupTestDB(t *testing.T) *DB {
timestamp INTEGER NOT NULL
);
CREATE VIEW packets_v AS
SELECT o.id, t.raw_hex,
strftime('%Y-%m-%dT%H:%M:%fZ', o.timestamp, 'unixepoch') AS timestamp,
obs.id AS observer_id, obs.name AS observer_name,
o.direction, o.snr, o.rssi, o.score, t.hash, t.route_type,
t.payload_type, t.payload_version, o.path_json, t.decoded_json,
t.created_at
FROM observations o
JOIN transmissions t ON t.id = o.transmission_id
LEFT JOIN observers obs ON obs.rowid = o.observer_idx;
`
if _, err := conn.Exec(schema); err != nil {
t.Fatal(err)
@@ -569,51 +559,6 @@ func TestGetNewTransmissionsSince(t *testing.T) {
}
}
func TestGetObservationsForHash(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
obs, err := db.GetObservationsForHash("abc123def4567890")
if err != nil {
t.Fatal(err)
}
if len(obs) != 2 {
t.Errorf("expected 2 observations, got %d", len(obs))
}
}
func TestGetPacketByIDFound(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
pkt, err := db.GetPacketByID(1)
if err != nil {
t.Fatal(err)
}
if pkt == nil {
t.Fatal("expected packet, got nil")
}
if pkt["hash"] != "abc123def4567890" {
t.Errorf("expected hash abc123def4567890, got %v", pkt["hash"])
}
}
func TestGetPacketByIDNotFound(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
pkt, err := db.GetPacketByID(9999)
if err != nil {
t.Fatal(err)
}
if pkt != nil {
t.Error("expected nil for nonexistent packet ID")
}
}
func TestGetTransmissionByIDFound(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
@@ -656,34 +601,6 @@ func TestGetPacketByHashNotFound(t *testing.T) {
}
}
func TestGetRecentPacketsForNode(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
packets, err := db.GetRecentPacketsForNode("aabbccdd11223344", "TestRepeater", 20)
if err != nil {
t.Fatal(err)
}
if len(packets) == 0 {
t.Error("expected packets for TestRepeater")
}
}
func TestGetRecentPacketsForNodeDefaultLimit(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
packets, err := db.GetRecentPacketsForNode("aabbccdd11223344", "TestRepeater", 0)
if err != nil {
t.Fatal(err)
}
if packets == nil {
t.Error("expected non-nil result")
}
}
func TestGetObserverIdsForRegion(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
@@ -733,46 +650,6 @@ func TestGetObserverIdsForRegion(t *testing.T) {
})
}
func TestGetNodeHealth(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
t.Run("found", func(t *testing.T) {
result, err := db.GetNodeHealth("aabbccdd11223344")
if err != nil {
t.Fatal(err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
node, ok := result["node"].(map[string]interface{})
if !ok {
t.Fatal("expected node object")
}
if node["name"] != "TestRepeater" {
t.Errorf("expected TestRepeater, got %v", node["name"])
}
stats, ok := result["stats"].(map[string]interface{})
if !ok {
t.Fatal("expected stats object")
}
if stats["totalPackets"] == nil {
t.Error("expected totalPackets in stats")
}
})
t.Run("not found", func(t *testing.T) {
result, err := db.GetNodeHealth("nonexistent")
if err != nil {
t.Fatal(err)
}
if result != nil {
t.Error("expected nil for nonexistent node")
}
})
}
func TestGetChannelMessages(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
@@ -815,48 +692,6 @@ func TestGetChannelMessages(t *testing.T) {
})
}
func TestGetTimestamps(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
t.Run("with results", func(t *testing.T) {
ts, err := db.GetTimestamps("2020-01-01")
if err != nil {
t.Fatal(err)
}
if len(ts) == 0 {
t.Error("expected timestamps")
}
})
t.Run("no results", func(t *testing.T) {
ts, err := db.GetTimestamps("2099-01-01")
if err != nil {
t.Fatal(err)
}
if len(ts) != 0 {
t.Errorf("expected 0 timestamps, got %d", len(ts))
}
})
}
func TestGetObservationCount(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
count := db.GetObservationCount("abc123def4567890")
if count != 2 {
t.Errorf("expected 2, got %d", count)
}
count = db.GetObservationCount("nonexistent")
if count != 0 {
t.Errorf("expected 0 for nonexistent, got %d", count)
}
}
func TestBuildPacketWhereFilters(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
@@ -1280,29 +1115,6 @@ func TestOpenDBInvalidPath(t *testing.T) {
}
}
func TestGetNodeHealthNoName(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Insert a node without a name
db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES ('obs1', 'Observer One', 'SJC')`)
db.conn.Exec(`INSERT INTO nodes (public_key, role, last_seen, first_seen, advert_count)
VALUES ('deadbeef12345678', 'repeater', '2026-01-15T10:00:00Z', '2026-01-01T00:00:00Z', 5)`)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DDEE', 'deadbeefhash1234', '2026-01-15T10:05:00Z', 1, 4,
'{"pubKey":"deadbeef12345678","type":"ADVERT"}')`)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 11.0, -91, '["dd"]', 1736935500)`)
result, err := db.GetNodeHealth("deadbeef12345678")
if err != nil {
t.Fatal(err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
}
func TestGetChannelMessagesObserverFallback(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
@@ -1383,20 +1195,6 @@ func TestQueryGroupedPacketsWithFilters(t *testing.T) {
}
}
func TestGetTracesEmpty(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
traces, err := db.GetTraces("nonexistenthash1")
if err != nil {
t.Fatal(err)
}
if len(traces) != 0 {
t.Errorf("expected 0 traces, got %d", len(traces))
}
}
func TestNullHelpers(t *testing.T) {
// nullStr
if nullStr(sql.NullString{Valid: false}) != nil {
@@ -1474,9 +1272,11 @@ func TestNodeTelemetryFields(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Insert node with telemetry data
db.conn.Exec(`INSERT INTO nodes (public_key, name, role, lat, lon, last_seen, first_seen, advert_count, battery_mv, temperature_c)
VALUES ('pk_telem1', 'SensorNode', 'sensor', 37.0, -122.0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', 5, 3700, 28.5)`)
// Test via GetNodeByPubkey
node, err := db.GetNodeByPubkey("pk_telem1")
if err != nil {
t.Fatal(err)
@@ -1491,6 +1291,7 @@ func TestNodeTelemetryFields(t *testing.T) {
t.Errorf("temperature_c=%v, want 28.5", node["temperature_c"])
}
// Test via GetNodes
nodes, _, _, err := db.GetNodes(50, 0, "sensor", "", "", "", "", "")
if err != nil {
t.Fatal(err)
@@ -1502,6 +1303,7 @@ func TestNodeTelemetryFields(t *testing.T) {
t.Errorf("GetNodes battery_mv=%v, want 3700", nodes[0]["battery_mv"])
}
// Test node without telemetry — fields should be nil
db.conn.Exec(`INSERT INTO nodes (public_key, name, role, last_seen, first_seen, advert_count)
VALUES ('pk_notelem', 'PlainNode', 'repeater', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', 3)`)
node2, _ := db.GetNodeByPubkey("pk_notelem")
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/meshcore-analyzer/server
module github.com/corescope/server
go 1.22
+6 -4
View File
@@ -63,7 +63,9 @@ func main() {
}
go func() {
log.Printf("[pprof] profiling UI at http://localhost:%s/debug/pprof/", pprofPort)
log.Fatal(http.ListenAndServe(":"+pprofPort, nil))
if err := http.ListenAndServe(":"+pprofPort, nil); err != nil {
log.Printf("[pprof] failed to start: %v (non-fatal)", err)
}
}()
}
@@ -114,7 +116,7 @@ func main() {
var tableName string
err = database.conn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='transmissions'").Scan(&tableName)
if err == sql.ErrNoRows {
log.Fatalf("[db] table 'transmissions' not found — is this a MeshCore Analyzer database?")
log.Fatalf("[db] table 'transmissions' not found — is this a CoreScope database?")
}
stats, err := database.GetStats()
@@ -153,7 +155,7 @@ func main() {
log.Printf("[static] directory %s not found — API-only mode", absPublic)
router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><body><h1>MeshCore Analyzer</h1><p>Frontend not found. API available at /api/</p></body></html>`))
w.Write([]byte(`<!DOCTYPE html><html><body><h1>CoreScope</h1><p>Frontend not found. API available at /api/</p></body></html>`))
})
}
@@ -180,7 +182,7 @@ func main() {
httpServer.Close()
}()
log.Printf("[server] MeshCore Analyzer (Go) listening on http://localhost:%d", cfg.Port)
log.Printf("[server] CoreScope (Go) listening on http://localhost:%d", cfg.Port)
if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("[server] %v", err)
}
+274 -797
View File
File diff suppressed because it is too large Load Diff
+38 -11
View File
@@ -18,7 +18,9 @@ func setupTestServer(t *testing.T) (*Server, *mux.Router) {
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
@@ -722,6 +724,9 @@ func TestNodePathsFound(t *testing.T) {
if body["paths"] == nil {
t.Error("expected paths in response")
}
if got, ok := body["totalTransmissions"].(float64); !ok || got < 1 {
t.Errorf("expected totalTransmissions >= 1, got %v", body["totalTransmissions"])
}
}
func TestNodePathsNotFound(t *testing.T) {
@@ -832,6 +837,9 @@ func TestObserverAnalytics(t *testing.T) {
if body["recentPackets"] == nil {
t.Error("expected recentPackets")
}
if recent, ok := body["recentPackets"].([]interface{}); !ok || len(recent) == 0 {
t.Errorf("expected non-empty recentPackets, got %v", body["recentPackets"])
}
})
t.Run("custom days", func(t *testing.T) {
@@ -1251,6 +1259,11 @@ func TestNodeAnalyticsNoNameNode(t *testing.T) {
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
@@ -1282,6 +1295,11 @@ func TestNodeHealthForNoNameNode(t *testing.T) {
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
@@ -1521,8 +1539,6 @@ func TestHandlerErrorPaths(t *testing.T) {
router := mux.NewRouter()
srv.RegisterRoutes(router)
// Drop the view to force query errors
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
t.Run("stats error", func(t *testing.T) {
db.conn.Exec("DROP TABLE IF EXISTS transmissions")
@@ -1563,7 +1579,7 @@ func TestHandlerErrorTraces(t *testing.T) {
router := mux.NewRouter()
srv.RegisterRoutes(router)
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
db.conn.Exec("DROP TABLE IF EXISTS observations")
req := httptest.NewRequest("GET", "/api/traces/abc123def4567890", nil)
w := httptest.NewRecorder()
@@ -1697,13 +1713,12 @@ func TestHandlerErrorTimestamps(t *testing.T) {
router := mux.NewRouter()
srv.RegisterRoutes(router)
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
// Without a store, timestamps returns empty 200
req := httptest.NewRequest("GET", "/api/packets/timestamps?since=2020-01-01", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 500 {
t.Errorf("expected 500 for timestamps error, got %d", w.Code)
if w.Code != 200 {
t.Errorf("expected 200 for timestamps without store, got %d", w.Code)
}
}
@@ -1740,8 +1755,8 @@ func TestHandlerErrorBulkHealth(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/bulk-health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 500 {
t.Errorf("expected 500, got %d", w.Code)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
@@ -1876,7 +1891,9 @@ func TestGetNodeHashSizeInfoFlipFlop(t *testing.T) {
db := setupTestDB(t)
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
pk := "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, role) VALUES (?, 'TestNode', 'repeater')", pk)
@@ -1934,7 +1951,17 @@ for _, field := range arrayFields {
if body[field] == nil {
t.Errorf("field %q is null, expected []", field)
}
}
}
func TestObserverAnalyticsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 503 {
t.Fatalf("expected 503, got %d", w.Code)
}
}
func min(a, b int) int {
if a < b {
+55 -48
View File
@@ -62,7 +62,7 @@ type StoreObs struct {
type PacketStore struct {
mu sync.RWMutex
db *DB
packets []*StoreTx // sorted by first_seen DESC
packets []*StoreTx // sorted by first_seen ASC (oldest first; newest at tail)
byHash map[string]*StoreTx // hash → *StoreTx
byTxID map[int]*StoreTx // transmission_id → *StoreTx
byObsID map[int]*StoreObs // observation_id → *StoreObs
@@ -176,7 +176,7 @@ func (s *PacketStore) Load() error {
FROM transmissions t
LEFT JOIN observations o ON o.transmission_id = t.id
LEFT JOIN observers obs ON obs.rowid = o.observer_idx
ORDER BY t.first_seen DESC, o.timestamp DESC`
ORDER BY t.first_seen ASC, o.timestamp DESC`
} else {
loadSQL = `SELECT t.id, t.raw_hex, t.hash, t.first_seen, t.route_type,
t.payload_type, t.payload_version, t.decoded_json,
@@ -184,7 +184,7 @@ func (s *PacketStore) Load() error {
o.snr, o.rssi, o.score, o.path_json, o.timestamp
FROM transmissions t
LEFT JOIN observations o ON o.transmission_id = t.id
ORDER BY t.first_seen DESC, o.timestamp DESC`
ORDER BY t.first_seen ASC, o.timestamp DESC`
}
rows, err := s.db.conn.Query(loadSQL)
@@ -368,28 +368,32 @@ func (s *PacketStore) QueryPackets(q PacketQuery) *PacketResult {
results := s.filterPackets(q)
total := len(results)
if q.Order == "ASC" {
sorted := make([]*StoreTx, len(results))
copy(sorted, results)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].FirstSeen < sorted[j].FirstSeen
})
results = sorted
}
// Paginate
// results is oldest-first (ASC). For DESC (default) read backwards from the tail;
// for ASC read forwards. Both are O(page_size) — no sort copy needed.
start := q.Offset
if start >= len(results) {
if start >= total {
return &PacketResult{Packets: []map[string]interface{}{}, Total: total}
}
end := start + q.Limit
if end > len(results) {
end = len(results)
pageSize := q.Limit
if start+pageSize > total {
pageSize = total - start
}
packets := make([]map[string]interface{}, 0, end-start)
for _, tx := range results[start:end] {
packets = append(packets, txToMap(tx))
packets := make([]map[string]interface{}, 0, pageSize)
if q.Order == "ASC" {
for _, tx := range results[start : start+pageSize] {
packets = append(packets, txToMap(tx))
}
} else {
// DESC: newest items are at the tail; page 0 = last pageSize items reversed
endIdx := total - start
startIdx := endIdx - pageSize
if startIdx < 0 {
startIdx = 0
}
for i := endIdx - 1; i >= startIdx; i-- {
packets = append(packets, txToMap(results[i]))
}
}
return &PacketResult{Packets: packets, Total: total}
}
@@ -719,15 +723,16 @@ func (s *PacketStore) GetTimestamps(since string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
// packets sorted newest first — scan from start until older than since
// packets sorted oldest-first — scan from tail until we reach items older than since
var result []string
for _, tx := range s.packets {
for i := len(s.packets) - 1; i >= 0; i-- {
tx := s.packets[i]
if tx.FirstSeen <= since {
break
}
result = append(result, tx.FirstSeen)
}
// Reverse to get ASC order
// result is currently newest-first; reverse to return ASC order
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
@@ -777,23 +782,30 @@ func (s *PacketStore) QueryMultiNodePackets(pubkeys []string, limit, offset int,
total := len(filtered)
if order == "ASC" {
sort.Slice(filtered, func(i, j int) bool {
return filtered[i].FirstSeen < filtered[j].FirstSeen
})
}
// filtered is oldest-first (built by iterating s.packets forward).
// Apply same DESC/ASC pagination logic as QueryPackets.
if offset >= total {
return &PacketResult{Packets: []map[string]interface{}{}, Total: total}
}
end := offset + limit
if end > total {
end = total
pageSize := limit
if offset+pageSize > total {
pageSize = total - offset
}
packets := make([]map[string]interface{}, 0, end-offset)
for _, tx := range filtered[offset:end] {
packets = append(packets, txToMap(tx))
packets := make([]map[string]interface{}, 0, pageSize)
if order == "ASC" {
for _, tx := range filtered[offset : offset+pageSize] {
packets = append(packets, txToMap(tx))
}
} else {
endIdx := total - offset
startIdx := endIdx - pageSize
if startIdx < 0 {
startIdx = 0
}
for i := endIdx - 1; i >= startIdx; i-- {
packets = append(packets, txToMap(filtered[i]))
}
}
return &PacketResult{Packets: packets, Total: total}
}
@@ -926,15 +938,14 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac
DecodedJSON: r.decodedJSON,
}
s.byHash[r.hash] = tx
// Prepend (newest first)
s.packets = append([]*StoreTx{tx}, s.packets...)
s.packets = append(s.packets, tx) // oldest-first; new items go to tail
s.byTxID[r.txID] = tx
s.indexByNode(tx)
if tx.PayloadType != nil {
pt := *tx.PayloadType
// Prepend to maintain newest-first order (matches Load ordering)
// Append to maintain oldest-first order (matches Load ordering)
// so GetChannelMessages reverse iteration stays correct
s.byPayloadType[pt] = append([]*StoreTx{tx}, s.byPayloadType[pt]...)
s.byPayloadType[pt] = append(s.byPayloadType[pt], tx)
}
if _, exists := broadcastTxs[r.txID]; !exists {
@@ -1079,8 +1090,6 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac
s.cacheMu.Unlock()
}
log.Printf("[poller] IngestNewFromDB: found %d new txs, maxID %d->%d", len(result), sinceID, newMaxID)
return result, newMaxID
}
@@ -1263,8 +1272,7 @@ func (s *PacketStore) IngestNewObservations(sinceObsID, limit int) int {
s.subpathCache = make(map[string]*cachedResult)
s.cacheMu.Unlock()
log.Printf("[poller] IngestNewObservations: updated %d existing txs, maxObsID %d->%d",
len(updatedTxs), sinceObsID, newMaxObsID)
// analytics caches cleared; no per-cycle log to avoid stdout overhead
}
return newMaxObsID
@@ -1888,7 +1896,7 @@ func (s *PacketStore) GetChannelMessages(channelHash string, limit, offset int)
msgMap := map[string]*msgEntry{}
var msgOrder []string
// Iterate type-5 packets oldest-first (byPayloadType is in load order = newest first)
// Iterate type-5 packets oldest-first (byPayloadType is ASC = oldest first)
type decodedMsg struct {
Type string `json:"type"`
Channel string `json:"channel"`
@@ -1899,8 +1907,7 @@ func (s *PacketStore) GetChannelMessages(channelHash string, limit, offset int)
}
grpTxts := s.byPayloadType[5]
for i := len(grpTxts) - 1; i >= 0; i-- {
tx := grpTxts[i]
for _, tx := range grpTxts {
if tx.DecodedJSON == "" {
continue
}
@@ -4069,13 +4076,13 @@ func (s *PacketStore) GetNodeHealth(pubkey string) (map[string]interface{}, erro
lhVal = lastHeard
}
// Recent packets (up to 20, newest first — packets are already sorted DESC)
// Recent packets (up to 20, newest first — read from tail of oldest-first slice)
recentLimit := 20
if len(packets) < recentLimit {
recentLimit = len(packets)
}
recentPackets := make([]map[string]interface{}, 0, recentLimit)
for i := 0; i < recentLimit; i++ {
for i := len(packets) - 1; i >= len(packets)-recentLimit; i-- {
p := txToMap(packets[i])
delete(p, "observations")
recentPackets = append(recentPackets, p)
+2 -2
View File
@@ -10,7 +10,7 @@
"key": "/path/to/key.pem"
},
"branding": {
"siteName": "MeshCore Analyzer",
"siteName": "CoreScope",
"tagline": "Real-time MeshCore LoRa mesh network analyzer",
"logoUrl": null,
"faviconUrl": null
@@ -32,7 +32,7 @@
"observer": "#8b5cf6"
},
"home": {
"heroTitle": "MeshCore Analyzer",
"heroTitle": "CoreScope",
"heroSubtitle": "Find your nodes to start monitoring them.",
"steps": [
{ "emoji": "📡", "title": "Connect", "description": "Link your node to the mesh" },
+9 -8
View File
@@ -1,10 +1,12 @@
# Volume paths unified with manage.sh — see manage.sh lines 9-12, 56-68, 98-113
# Volume paths unified with manage.sh — see manage.sh lines 9-12, 55-62, 920, 944
# 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:
image: meshcore-analyzer:latest
container_name: meshcore-prod
image: corescope:latest
container_name: corescope-prod
restart: unless-stopped
ports:
- "${PROD_HTTP_PORT:-80}:${PROD_HTTP_PORT:-80}"
@@ -24,8 +26,8 @@ services:
retries: 3
staging:
image: meshcore-analyzer:latest
container_name: meshcore-staging
image: corescope:latest
container_name: corescope-staging
restart: unless-stopped
ports:
- "${STAGING_HTTP_PORT:-81}:${STAGING_HTTP_PORT:-81}"
@@ -52,8 +54,8 @@ services:
args:
APP_VERSION: ${APP_VERSION:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
image: meshcore-go:latest
container_name: meshcore-staging-go
image: corescope-go:latest
container_name: corescope-staging-go
restart: unless-stopped
ports:
- "${STAGING_GO_HTTP_PORT:-82}:80"
@@ -67,7 +69,6 @@ services:
environment:
- NODE_ENV=staging
- ENABLE_PPROF=true
- PPROF_PORT=6060
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/stats"]
interval: 30s
+4 -4
View File
@@ -14,8 +14,8 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:meshcore-ingestor]
command=/app/meshcore-ingestor -config /app/config.json
[program:corescope-ingestor]
command=/app/corescope-ingestor -config /app/config.json
directory=/app
autostart=true
autorestart=true
@@ -24,8 +24,8 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:meshcore-server]
command=/app/meshcore-server -config-dir /app -db /app/data/meshcore.db -public /app/public -port 3000
[program:corescope-server]
command=/app/corescope-server -config-dir /app -db /app/data/meshcore.db -public /app/public -port 3000
directory=/app
autostart=true
autorestart=true
+1 -1
View File
@@ -14,7 +14,7 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:meshcore-analyzer]
[program:corescope]
command=node /app/server.js
directory=/app
autostart=true
+1 -1
View File
@@ -27,7 +27,7 @@ No restart needed. The server picks up changes to `theme.json` on every page loa
**Bare metal / PM2 / systemd:**
```bash
# Same directory as server.js and config.json
cp theme.json /path/to/meshcore-analyzer/
cp theme.json /path/to/corescope/
```
Check the server logs on startup — it tells you where it's looking:
+22 -22
View File
@@ -1,6 +1,6 @@
# Deploying MeshCore Analyzer
# Deploying CoreScope
Get MeshCore Analyzer running with automatic HTTPS on your own server.
Get CoreScope running with automatic HTTPS on your own server.
## Table of Contents
@@ -19,7 +19,7 @@ Get MeshCore Analyzer running with automatic HTTPS on your own server.
## What You'll End Up With
- MeshCore Analyzer running at `https://your-domain.com`
- CoreScope running at `https://your-domain.com`
- Automatic HTTPS certificates (via Let's Encrypt + Caddy)
- Built-in MQTT broker for receiving packets from observers
- SQLite database for packet storage (auto-created)
@@ -83,8 +83,8 @@ docker --version
The easiest way — use the management script:
```bash
git clone https://github.com/Kpa-clawbot/meshcore-analyzer.git
cd meshcore-analyzer
git clone https://github.com/Kpa-clawbot/corescope.git
cd corescope
./manage.sh setup
```
@@ -111,8 +111,8 @@ flowchart LR
### 1. Download the code
```bash
git clone https://github.com/Kpa-clawbot/meshcore-analyzer.git
cd meshcore-analyzer
git clone https://github.com/Kpa-clawbot/corescope.git
cd corescope
```
### 2. Create your config
@@ -153,10 +153,10 @@ Save and close. Caddy handles certificates, renewals, and HTTP→HTTPS redirects
### 4. Build and run
```bash
docker build -t meshcore-analyzer .
docker build -t corescope .
docker run -d \
--name meshcore-analyzer \
--name corescope \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
@@ -164,7 +164,7 @@ docker run -d \
-v $(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro \
-v meshcore-data:/app/data \
-v caddy-data:/data/caddy \
meshcore-analyzer
corescope
```
What each flag does:
@@ -184,12 +184,12 @@ Open `https://your-domain.com`. You should see the analyzer home page.
Check the logs:
```bash
docker logs meshcore-analyzer
docker logs corescope
```
Expected output:
```
MeshCore Analyzer running on http://localhost:3000
CoreScope running on http://localhost:3000
MQTT [local] connected to mqtt://localhost:1883
[pre-warm] 12 endpoints in XXXms
```
@@ -215,7 +215,7 @@ Add a remote broker to `mqttSources` in your `config.json`:
}
```
Restart: `docker restart meshcore-analyzer`
Restart: `docker restart corescope`
### Option B: Run your own observer
@@ -271,12 +271,12 @@ If you already run a reverse proxy, skip Caddy entirely and proxy directly to th
```bash
docker run -d \
--name meshcore-analyzer \
--name corescope \
--restart unless-stopped \
-p 3000:3000 \
-v $(pwd)/config.json:/app/config.json:ro \
-v meshcore-data:/app/data \
meshcore-analyzer
corescope
```
Then configure your existing proxy to forward traffic to `localhost:3000`.
@@ -287,12 +287,12 @@ For local testing or a LAN-only setup, use the default Caddyfile that ships in t
```bash
docker run -d \
--name meshcore-analyzer \
--name corescope \
--restart unless-stopped \
-p 80:80 \
-v $(pwd)/config.json:/app/config.json:ro \
-v meshcore-data:/app/data \
meshcore-analyzer
corescope
```
## MQTT Security
@@ -315,7 +315,7 @@ password_file /etc/mosquitto/passwd
```
After starting the container, create users:
```bash
docker exec -it meshcore-analyzer mosquitto_passwd -c /etc/mosquitto/passwd myuser
docker exec -it corescope mosquitto_passwd -c /etc/mosquitto/passwd myuser
```
**Option 3: Use TLS** — For production, configure Mosquitto with TLS certificates. See the [Mosquitto docs](https://mosquitto.org/man/mosquitto-conf-5.html).
@@ -331,7 +331,7 @@ Packet data is stored in `meshcore.db` inside the data volume.
**Using manage.sh (easiest):**
```bash
./manage.sh backup # Saves to ./backups/meshcore-TIMESTAMP.db
./manage.sh backup # Saves to ./backups/corescope-TIMESTAMP/
./manage.sh backup ~/my-backup.db # Custom path
./manage.sh restore ./backups/some-file.db # Restore (backs up current DB first)
```
@@ -345,7 +345,7 @@ If you used `-v ./analyzer-data:/app/data` instead of a Docker volume, the datab
```bash
crontab -e
# Add:
0 3 * * * cd /path/to/meshcore-analyzer && ./manage.sh backup
0 3 * * * cd /path/to/corescope && ./manage.sh backup
```
## Updating
@@ -398,11 +398,11 @@ Center the map on your area in `config.json`:
| Problem | Likely cause | Fix |
|---------|-------------|-----|
| Site shows "connection refused" | Container not running | `docker ps` to check, `docker logs meshcore-analyzer` for errors |
| Site shows "connection refused" | Container not running | `docker ps` to check, `docker logs corescope` for errors |
| HTTPS not working | Port 80 blocked | Open port 80 — Caddy needs it for ACME challenges |
| "too many certificates" error | Let's Encrypt rate limit (5/domain/week) | Use a different subdomain, bring your own cert, or wait a week |
| Certificate won't provision | DNS not pointed at server | `dig your-domain` must show your server IP before starting |
| No packets appearing | No observer connected | `docker exec meshcore-analyzer mosquitto_sub -t 'meshcore/#' -C 1 -W 10` — if silent, no data is coming in |
| No packets appearing | No observer connected | `docker exec corescope mosquitto_sub -t 'meshcore/#' -C 1 -W 10` — if silent, no data is coming in |
| Container crashes on startup | Bad JSON in config | `python3 -c "import json; json.load(open('config.json'))"` to validate |
| "address already in use" | Another web server on 80/443 | Stop it: `sudo systemctl stop nginx apache2` |
| Slow on Raspberry Pi | First build is slow | Normal — subsequent builds use cache. Runtime performance is fine. |
+1 -1
View File
@@ -1,4 +1,4 @@
# Hash Prefix Disambiguation in MeshCore Analyzer
# Hash Prefix Disambiguation in CoreScope
## Section 1: Executive Summary
+2 -2
View File
@@ -1,4 +1,4 @@
# MeshCore Analyzer — API Contract Specification
# CoreScope — API Contract Specification
> **Authoritative contract.** Both the Node.js and Go backends MUST conform to this spec.
> The frontend relies on these exact shapes. Breaking changes require a spec update first.
@@ -1547,7 +1547,7 @@ Theme and branding configuration (merged from config.json + theme.json).
```jsonc
{
"branding": {
"siteName": string, // default: "MeshCore Analyzer"
"siteName": string, // default: "CoreScope"
"tagline": string // default: "Real-time MeshCore LoRa mesh network analyzer"
// ... additional branding keys from config/theme files
},
+17 -17
View File
@@ -1,6 +1,6 @@
# Migrating from Node.js to Go Engine
Guide for existing MeshCore Analyzer users switching from the Node.js Docker image to the Go version.
Guide for existing CoreScope users switching from the Node.js Docker image to the Go version.
> **Status (July 2025):** The Go engine is fully functional for production use.
> Go images are **not yet published to Docker Hub** — you build locally from source.
@@ -24,11 +24,11 @@ Guide for existing MeshCore Analyzer users switching from the Node.js Docker ima
## Prerequisites
- **Docker** 20.10+ and **Docker Compose** v2 (verify: `docker compose version`)
- An existing MeshCore Analyzer deployment running the Node.js image
- An existing CoreScope deployment running the Node.js image
- The repository cloned locally (needed to build the Go image):
```bash
git clone https://github.com/meshcore-dev/meshcore-analyzer.git
cd meshcore-analyzer
git clone https://github.com/Kpa-clawbot/meshcore-analyzer.git
cd corescope
git pull # get latest
```
- Your `config.json` and `caddy-config/Caddyfile` in place (the same ones you use now)
@@ -122,7 +122,7 @@ docker compose --profile staging-go build staging-go
Or build directly:
```bash
docker build -f Dockerfile.go -t meshcore-go:latest \
docker build -f Dockerfile.go -t corescope-go:latest \
--build-arg APP_VERSION=$(git describe --tags 2>/dev/null || echo unknown) \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo unknown) \
.
@@ -151,7 +151,7 @@ Once satisfied, update `docker-compose.yml` to use the Go image for prod:
```yaml
services:
prod:
image: meshcore-go:latest # was: meshcore-analyzer:latest
image: corescope-go:latest # was: corescope:latest
build:
context: .
dockerfile: Dockerfile.go # add this
@@ -174,9 +174,9 @@ docker compose up -d prod
./manage.sh stop
# Build the Go image
docker build -f Dockerfile.go -t meshcore-analyzer:latest .
docker build -f Dockerfile.go -t corescope:latest .
# Start (manage.sh uses the meshcore-analyzer:latest image)
# Start (manage.sh uses the corescope:latest image)
./manage.sh start
```
@@ -248,7 +248,7 @@ These should match (or be close to) your pre-migration numbers.
```bash
# Watch container logs for MQTT messages
docker logs -f meshcore-prod --tail 20
docker logs -f corescope-prod --tail 20
# Or use manage.sh
./manage.sh mqtt-test
@@ -279,13 +279,13 @@ If something goes wrong, switching back is straightforward:
```yaml
services:
prod:
image: meshcore-analyzer:latest # back to Node.js
image: corescope:latest # back to Node.js
# Remove the build.dockerfile line if you added it
```
```bash
# Rebuild Node.js image if needed
docker build -t meshcore-analyzer:latest .
docker build -t corescope:latest .
docker compose up -d --force-recreate prod
```
@@ -295,8 +295,8 @@ docker compose up -d --force-recreate prod
```bash
./manage.sh stop
# Rebuild Node.js image (overwrites the meshcore-analyzer:latest tag)
docker build -t meshcore-analyzer:latest .
# Rebuild Node.js image (overwrites the corescope:latest tag)
docker build -t corescope:latest .
./manage.sh start
```
@@ -310,9 +310,9 @@ docker build -t meshcore-analyzer:latest .
Or manually:
```bash
docker stop meshcore-prod
docker stop corescope-prod
cp backups/pre-go-migration/meshcore.db ~/meshcore-data/meshcore.db
docker start meshcore-prod
docker start corescope-prod
```
---
@@ -348,7 +348,7 @@ docker start meshcore-prod
|------|---------|-----|
| `engine` field in `/api/health` | Not present or `"node"` | Always `"go"` |
| MQTT URL scheme | Uses `mqtt://` / `mqtts://` natively | Auto-converts to `tcp://` / `ssl://` (transparent) |
| Process model | Single Node.js process (server + ingestor) | Two binaries: `meshcore-ingestor` + `meshcore-server` (managed by supervisord) |
| Process model | Single Node.js process (server + ingestor) | Two binaries: `corescope-ingestor` + `corescope-server` (managed by supervisord) |
| Memory management | Configurable via `packetStore.maxMemoryMB` | Loads all packets; no configurable limit |
| Startup time | Faster (no compilation) | Slightly slower (loads all packets from DB into memory) |
@@ -393,4 +393,4 @@ The following gaps have been identified. Check the GitHub issue tracker for curr
3. **Go ingestor missing `meshcore/self_info` handling** — The local node identity topic is not processed. Low impact but breaks parity.
4. **No Docker Hub publishing for Go images** — Users must build locally. CI/CD pipeline should publish `meshcore-go:latest` alongside the Node.js image.
4. **No Docker Hub publishing for Go images** — Users must build locally. CI/CD pipeline should publish `corescope-go:latest` alongside the Node.js image.
+47 -56
View File
@@ -1,13 +1,13 @@
#!/bin/bash
# MeshCore Analyzer — Setup & Management Helper
# CoreScope — Setup & Management Helper
# Usage: ./manage.sh [command]
#
# Idempotent: safe to cancel and re-run at any point.
# Each step checks what's already done and skips it.
set -e
CONTAINER_NAME="meshcore-analyzer"
IMAGE_NAME="meshcore-analyzer"
CONTAINER_NAME="corescope"
IMAGE_NAME="corescope"
DATA_VOLUME="meshcore-data"
CADDY_VOLUME="caddy-data"
STATE_FILE=".setup-state"
@@ -52,19 +52,12 @@ is_done() { [ -f "$STATE_FILE" ] && grep -qx "$1" "$STATE_FILE" 2>/dev/null;
# ─── Helpers ──────────────────────────────────────────────────────────────
# Determine the correct data volume/mount args for docker run.
# Detects existing host data directories and uses bind mounts if found.
# Always uses bind mounts so the DB is visible on the filesystem.
get_data_mount_args() {
# Check for existing host data directories with a DB file
if [ -d "$HOME/meshcore-data" ] && [ -f "$HOME/meshcore-data/meshcore.db" ]; then
echo "-v $HOME/meshcore-data:/app/data"
return
fi
if [ -d "$(pwd)/data" ] && [ -f "$(pwd)/data/meshcore.db" ]; then
echo "-v $(pwd)/data:/app/data"
return
fi
# Default: Docker named volume
echo "-v ${DATA_VOLUME}:/app/data"
# Always use bind mount (from .env or default)
# Create the directory if it doesn't exist
mkdir -p "$PROD_DATA"
echo "-v $PROD_DATA:/app/data"
}
# Determine the required port mappings from Caddyfile
@@ -201,7 +194,7 @@ TOTAL_STEPS=6
cmd_setup() {
echo ""
echo "═══════════════════════════════════════"
echo " MeshCore Analyzer Setup"
echo " CoreScope Setup"
echo "═══════════════════════════════════════"
echo ""
@@ -385,8 +378,8 @@ cmd_setup() {
step 5 "Starting container"
# Detect existing data directories
if [ -d "$HOME/meshcore-data" ] && [ -f "$HOME/meshcore-data/meshcore.db" ]; then
info "Found existing data at \$HOME/meshcore-data/ — will use bind mount."
if [ -d "$PROD_DATA" ] && [ -f "$PROD_DATA/meshcore.db" ]; then
info "Found existing data at $PROD_DATA/ — will use bind mount."
elif [ -d "$(pwd)/data" ] && [ -f "$(pwd)/data/meshcore.db" ]; then
info "Found existing data at ./data/ — will use bind mount."
fi
@@ -501,7 +494,7 @@ prepare_staging_config() {
if [ ! -f "$staging_config" ] || [ "$prod_config" -nt "$staging_config" ]; then
info "Copying production config to staging..."
cp "$prod_config" "$staging_config"
sed -i 's/"siteName":\s*"[^"]*"/"siteName": "MeshCore Analyzer — STAGING"/' "$staging_config"
sed -i 's/"siteName":\s*"[^"]*"/"siteName": "CoreScope — STAGING"/' "$staging_config"
log "Staging config created at ${staging_config} with STAGING site name."
else
log "Staging config is up to date."
@@ -541,13 +534,13 @@ cmd_start() {
prepare_staging_db
prepare_staging_config
info "Starting production container (meshcore-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
info "Starting staging container (meshcore-staging) on port ${STAGING_HTTP_PORT:-81}..."
info "Starting production container (corescope-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
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_HTTP_PORT:-81} (MQTT: ${STAGING_MQTT_PORT:-1884})"
else
info "Starting production container (meshcore-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
info "Starting production container (corescope-prod) on ports ${PROD_HTTP_PORT:-80}/${PROD_HTTPS_PORT:-443}..."
docker compose up -d prod
log "Production started. Staging NOT running (use --with-staging to start both)."
fi
@@ -586,12 +579,12 @@ cmd_stop() {
if $COMPOSE_MODE; then
case "$TARGET" in
prod)
info "Stopping production container (meshcore-prod)..."
info "Stopping production container (corescope-prod)..."
docker compose stop prod
log "Production stopped."
;;
staging)
info "Stopping staging container (meshcore-staging)..."
info "Stopping staging container (corescope-staging)..."
docker compose stop staging
log "Staging stopped."
;;
@@ -617,12 +610,12 @@ cmd_restart() {
local TARGET="${1:-prod}"
case "$TARGET" in
prod)
info "Restarting production container (meshcore-prod)..."
info "Restarting production container (corescope-prod)..."
docker compose up -d --force-recreate prod
log "Production restarted."
;;
staging)
info "Restarting staging container (meshcore-staging)..."
info "Restarting staging container (corescope-staging)..."
docker compose --profile staging up -d --force-recreate staging
log "Staging restarted."
;;
@@ -698,19 +691,19 @@ cmd_status() {
if $COMPOSE_MODE; then
echo "═══════════════════════════════════════"
echo " MeshCore Analyzer Status (Compose)"
echo " CoreScope Status (Compose)"
echo "═══════════════════════════════════════"
echo ""
# Production
show_container_status "meshcore-prod" "Production"
show_container_status "corescope-prod" "Production"
echo ""
# Staging
if container_running "meshcore-staging"; then
show_container_status "meshcore-staging" "Staging"
if container_running "corescope-staging"; then
show_container_status "corescope-staging" "Staging"
else
info "Staging (meshcore-staging): Not running (use --with-staging to start both)"
info "Staging (corescope-staging): Not running (use --with-staging to start both)"
fi
echo ""
@@ -804,7 +797,7 @@ cmd_logs() {
docker compose logs -f --tail="$LINES" prod
;;
staging)
if container_running "meshcore-staging"; then
if container_running "corescope-staging"; then
info "Tailing staging logs..."
docker compose logs -f --tail="$LINES" staging
else
@@ -843,10 +836,10 @@ cmd_promote() {
# Show what's currently running
local staging_image staging_created prod_image prod_created
staging_image=$(docker inspect meshcore-staging --format '{{.Config.Image}}' 2>/dev/null || echo "not running")
staging_created=$(docker inspect meshcore-staging --format '{{.Created}}' 2>/dev/null || echo "N/A")
prod_image=$(docker inspect meshcore-prod --format '{{.Config.Image}}' 2>/dev/null || echo "not running")
prod_created=$(docker inspect meshcore-prod --format '{{.Created}}' 2>/dev/null || echo "N/A")
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")
echo " Staging: ${staging_image} (created ${staging_created})"
echo " Prod: ${prod_image} (created ${prod_created})"
@@ -863,8 +856,8 @@ cmd_promote() {
mkdir -p "$BACKUP_DIR"
if [ -f "$PROD_DATA/meshcore.db" ]; then
cp "$PROD_DATA/meshcore.db" "$BACKUP_DIR/"
elif container_running "meshcore-prod"; then
docker cp meshcore-prod:/app/data/meshcore.db "$BACKUP_DIR/"
elif container_running "corescope-prod"; then
docker cp corescope-prod:/app/data/meshcore.db "$BACKUP_DIR/"
else
warn "Could not backup production database."
fi
@@ -878,7 +871,7 @@ cmd_promote() {
info "Waiting for production health check..."
local i health
for i in $(seq 1 30); do
health=$(container_health "meshcore-prod")
health=$(container_health "corescope-prod")
if [ "$health" = "healthy" ]; then
log "Production healthy after ${i}s"
break
@@ -918,13 +911,14 @@ cmd_update() {
cmd_backup() {
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="${1:-./backups/meshcore-${TIMESTAMP}}"
BACKUP_DIR="${1:-./backups/corescope-${TIMESTAMP}}"
mkdir -p "$BACKUP_DIR"
info "Backing up to ${BACKUP_DIR}/"
# Database
DB_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/meshcore.db
# Always use bind mount path (from .env or default)
DB_PATH="$PROD_DATA/meshcore.db"
if [ -f "$DB_PATH" ]; then
cp "$DB_PATH" "$BACKUP_DIR/meshcore.db"
log "Database ($(du -h "$BACKUP_DIR/meshcore.db" | cut -f1))"
@@ -948,7 +942,8 @@ cmd_backup() {
fi
# Theme
THEME_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/theme.json
# Always use bind mount path (from .env or default)
THEME_PATH="$PROD_DATA/theme.json"
if [ -f "$THEME_PATH" ]; then
cp "$THEME_PATH" "$BACKUP_DIR/theme.json"
log "theme.json"
@@ -972,7 +967,7 @@ cmd_restore() {
if [ -d "./backups" ]; then
echo ""
echo " Available backups:"
ls -dt ./backups/meshcore-* 2>/dev/null | head -10 | while read d; do
ls -dt ./backups/meshcore-* ./backups/corescope-* 2>/dev/null | head -10 | while read d; do
if [ -d "$d" ]; then
echo " $d/ ($(ls "$d" | wc -l) files)"
elif [ -f "$d" ]; then
@@ -1019,17 +1014,15 @@ cmd_restore() {
# Backup current state first
info "Backing up current state..."
cmd_backup "./backups/meshcore-pre-restore-$(date +%Y%m%d-%H%M%S)"
cmd_backup "./backups/corescope-pre-restore-$(date +%Y%m%d-%H%M%S)"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
# Restore database
DEST_DB=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/meshcore.db
if [ -d "$(dirname "$DEST_DB")" ]; then
cp "$DB_FILE" "$DEST_DB"
else
docker cp "$DB_FILE" "${CONTAINER_NAME}:/app/data/meshcore.db"
fi
# Always use bind mount path (from .env or default)
mkdir -p "$PROD_DATA"
DEST_DB="$PROD_DATA/meshcore.db"
cp "$DB_FILE" "$DEST_DB"
log "Database restored"
# Restore config if present
@@ -1047,10 +1040,8 @@ cmd_restore() {
# Restore theme if present
if [ -n "$THEME_FILE" ] && [ -f "$THEME_FILE" ]; then
DEST_THEME=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/theme.json
if [ -d "$(dirname "$DEST_THEME")" ]; then
cp "$THEME_FILE" "$DEST_THEME"
fi
DEST_THEME="$PROD_DATA/theme.json"
cp "$THEME_FILE" "$DEST_THEME"
log "theme.json restored"
fi
@@ -1098,14 +1089,14 @@ cmd_reset() {
rm -f "$STATE_FILE"
log "Reset complete. Run './manage.sh setup' to start over."
echo " Data volume preserved. To delete it: docker volume rm ${DATA_VOLUME}"
echo " Data directory: $PROD_DATA (not removed)"
}
# ─── Help ─────────────────────────────────────────────────────────────────
cmd_help() {
echo ""
echo "MeshCore Analyzer — Management Script"
echo "CoreScope — Management Script"
echo ""
echo "Usage: ./manage.sh <command>"
echo ""
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "common.proto";
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
// Core Channel Type
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
// Pagination
+2 -2
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
//
// GET /api/config/theme Theme and branding configuration
@@ -10,7 +10,7 @@ option go_package = "github.com/meshcore-analyzer/proto/v1";
// Site branding configuration.
message Branding {
// Site name (default: "MeshCore Analyzer").
// Site name (default: "CoreScope").
string site_name = 1 [json_name = "siteName"];
// Site tagline.
string tagline = 2;
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
// Decoded Packet Structure
// Returned by POST /api/decode, POST /api/packets, and WS broadcast.
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "common.proto";
import "packet.proto";
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "common.proto";
import "packet.proto";
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "common.proto";
import "decoded.proto";
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "common.proto";
+1 -1
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package meshcore.v1;
option go_package = "github.com/meshcore-analyzer/proto/v1";
option go_package = "github.com/corescope/proto/v1";
import "decoded.proto";
import "packet.proto";
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — analytics.js (v2 — full nerd mode) === */
/* === CoreScope — analytics.js (v2 — full nerd mode) === */
'use strict';
(function () {
+2 -2
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — app.js === */
/* === CoreScope — app.js === */
'use strict';
// --- Route/Payload name maps ---
@@ -109,7 +109,7 @@ function formatVersionBadge(version, commit, engine) {
if (!version && !commit && !engine) return '';
var port = (typeof location !== 'undefined' && location.port) || '';
var isProd = !port || port === '80' || port === '443';
var GH = 'https://github.com/Kpa-clawbot/meshcore-analyzer';
var GH = 'https://github.com/Kpa-clawbot/corescope';
var parts = [];
if (version && isProd) {
var vTag = version.charAt(0) === 'v' ? version : 'v' + version;
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — audio-lab.js === */
/* === CoreScope — audio-lab.js === */
/* Audio Lab: Packet Jukebox for sound debugging & understanding */
'use strict';
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — channels.js === */
/* === CoreScope — channels.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — compare.js === */
/* === CoreScope — compare.js === */
/* Observer packet comparison — Fixes #129 */
'use strict';
+3 -3
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — customize.js === */
/* === CoreScope — customize.js === */
/* Tools → Customization: visual config builder with live preview & JSON export */
'use strict';
@@ -9,7 +9,7 @@
const DEFAULTS = {
branding: {
siteName: 'MeshCore Analyzer',
siteName: 'CoreScope',
tagline: 'Real-time MeshCore LoRa mesh network analyzer',
logoUrl: '',
faviconUrl: ''
@@ -45,7 +45,7 @@
ANON_REQ: '#f43f5e'
},
home: {
heroTitle: 'MeshCore Analyzer',
heroTitle: 'CoreScope',
heroSubtitle: 'Find your nodes to start monitoring them.',
steps: [
{ emoji: '💬', title: 'Join the Bay Area MeshCore Discord', description: 'The community Discord is the best place to get help and find local mesh enthusiasts.' },
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — home.css === */
/* === CoreScope — home.css === */
/* Override #app overflow:hidden for home page scrolling */
#app:has(.home-hero), #app:has(.home-chooser) { overflow-y: auto; }
+4 -4
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — home.js (My Mesh Dashboard) === */
/* === CoreScope — home.js (My Mesh Dashboard) === */
'use strict';
(function () {
@@ -39,7 +39,7 @@
function showChooser(container) {
container.innerHTML = `
<section class="home-chooser">
<h1>Welcome to ${escapeHtml(window.SITE_CONFIG?.branding?.siteName || 'MeshCore Analyzer')}</h1>
<h1>Welcome to ${escapeHtml(window.SITE_CONFIG?.branding?.siteName || 'CoreScope')}</h1>
<p>How familiar are you with MeshCore?</p>
<div class="chooser-options">
<button class="chooser-btn new" id="chooseNew">
@@ -63,7 +63,7 @@
const myNodes = getMyNodes();
const hasNodes = myNodes.length > 0;
const homeCfg = window.SITE_CONFIG?.home || null;
const siteName = window.SITE_CONFIG?.branding?.siteName || 'MeshCore Analyzer';
const siteName = window.SITE_CONFIG?.branding?.siteName || 'CoreScope';
container.innerHTML = `
<section class="home-hero">
@@ -324,7 +324,7 @@
loadMyNodes();
// Update title if no nodes left
const h1 = document.querySelector('.home-hero h1');
if (h1 && !getMyNodes().length) h1.textContent = 'MeshCore Analyzer';
if (h1 && !getMyNodes().length) h1.textContent = 'CoreScope';
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — hop-display.js === */
/* === CoreScope — hop-display.js === */
/* Shared hop rendering with conflict info for all pages */
'use strict';
+33 -33
View File
@@ -5,12 +5,12 @@
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>MeshCore Analyzer</title>
<title>CoreScope</title>
<!-- Open Graph / Discord embed -->
<meta property="og:title" content="MeshCore Analyzer">
<meta property="og:title" content="CoreScope">
<meta property="og:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, route analysis, and deep mesh analytics.">
<meta property="og:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
<meta property="og:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="https://analyzer.00id.net">
@@ -19,12 +19,12 @@
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="MeshCore Analyzer">
<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/meshcore-analyzer/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774690966">
<link rel="stylesheet" href="home.css?v=1774690966">
<link rel="stylesheet" href="live.css?v=1774690966">
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774731523">
<link rel="stylesheet" href="home.css?v=1774731523">
<link rel="stylesheet" href="live.css?v=1774731523">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -40,7 +40,7 @@
<div class="nav-left">
<a href="#/" class="nav-brand">
<span class="brand-icon">🍄</span>
<span class="brand-text">MeshCore Analyzer</span>
<span class="brand-text">CoreScope</span>
<span class="live-dot" id="liveDot" title="WebSocket connected" aria-label="WebSocket connected"></span>
</a>
<div class="nav-links">
@@ -81,29 +81,29 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1774690966"></script>
<script src="customize.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774690966"></script>
<script src="hop-resolver.js?v=1774690966"></script>
<script src="hop-display.js?v=1774690966"></script>
<script src="app.js?v=1774690966"></script>
<script src="home.js?v=1774690966"></script>
<script src="packet-filter.js?v=1774690966"></script>
<script src="packets.js?v=1774690966"></script>
<script src="map.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774690966" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1774731523"></script>
<script src="customize.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774731523"></script>
<script src="hop-resolver.js?v=1774731523"></script>
<script src="hop-display.js?v=1774731523"></script>
<script src="app.js?v=1774731523"></script>
<script src="home.js?v=1774731523"></script>
<script src="packet-filter.js?v=1774731523"></script>
<script src="packets.js?v=1774731523"></script>
<script src="map.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774731523" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — map.js === */
/* === CoreScope — map.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — node-analytics.js === */
/* === CoreScope — node-analytics.js === */
'use strict';
(function () {
const PAYLOAD_LABELS = { 0: 'Request', 1: 'Response', 2: 'Direct Msg', 3: 'ACK', 4: 'Advert', 5: 'Channel Msg', 7: 'Anon Req', 8: 'Path', 9: 'Trace', 11: 'Control' };
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — nodes.js === */
/* === CoreScope — nodes.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — observer-detail.js === */
/* === CoreScope — observer-detail.js === */
'use strict';
(function () {
const PAYLOAD_LABELS = { 0: 'Request', 1: 'Response', 2: 'Direct Msg', 3: 'ACK', 4: 'Advert', 5: 'Channel Msg', 7: 'Anon Req', 8: 'Path', 9: 'Trace', 11: 'Control' };
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — observers.js === */
/* === CoreScope — observers.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — packets.js === */
/* === CoreScope — packets.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — perf.js === */
/* === CoreScope — perf.js === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — region-filter.js (shared region filter component) === */
/* === CoreScope — region-filter.js (shared region filter component) === */
'use strict';
(function () {
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — roles.js (shared config module) === */
/* === CoreScope — roles.js (shared config module) === */
'use strict';
/*
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — style.css === */
/* === CoreScope — style.css === */
:root {
--nav-bg: #0f0f23;
+1 -1
View File
@@ -1,4 +1,4 @@
/* === MeshCore Analyzer — traces.js === */
/* === CoreScope — traces.js === */
'use strict';
(function () {
+3 -3
View File
@@ -344,7 +344,7 @@ app.get('/api/config/theme', (req, res) => {
const theme = loadThemeFile();
res.json({
branding: {
siteName: 'MeshCore Analyzer',
siteName: 'CoreScope',
tagline: 'Real-time MeshCore LoRa mesh network analyzer',
...(cfg.branding || {}),
...(theme.branding || {})
@@ -2956,7 +2956,7 @@ app.get('/{*splat}', (req, res) => {
if (fs.existsSync(indexPath)) {
res.sendFile(indexPath);
} else {
res.status(200).send('<!DOCTYPE html><html><body><h1>MeshCore Analyzer</h1><p>Frontend not yet built.</p></body></html>');
res.status(200).send('<!DOCTYPE html><html><body><h1>CoreScope</h1><p>Frontend not yet built.</p></body></html>');
}
});
@@ -2967,7 +2967,7 @@ if (require.main === module) {
db.removePhantomNodes();
server.listen(listenPort, () => {
const protocol = isHttps ? 'https' : 'http';
console.log(`MeshCore Analyzer running on ${protocol}://localhost:${listenPort}`);
console.log(`CoreScope running on ${protocol}://localhost:${listenPort}`);
// Log theme file location
let themeFound = false;
for (const p of THEME_PATHS) {
+1 -1
View File
@@ -3,7 +3,7 @@
set -e
echo "═══════════════════════════════════════"
echo " MeshCore Analyzer — Test Suite"
echo " CoreScope — Test Suite"
echo "═══════════════════════════════════════"
echo ""
+1 -1
View File
@@ -44,7 +44,7 @@ async function run() {
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('nav, .navbar, .nav, [class*="nav"]');
const title = await page.title();
assert(title.toLowerCase().includes('meshcore'), `Title "${title}" doesn't contain MeshCore`);
assert(title.toLowerCase().includes('corescope'), `Title "${title}" doesn't contain CoreScope`);
const nav = await page.$('nav, .navbar, .nav, [class*="nav"]');
assert(nav, 'Nav bar not found');
});
+1 -1
View File
@@ -1304,7 +1304,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
loadInCtx(ctx, 'public/app.js');
return ctx;
}
const GH = 'https://github.com/Kpa-clawbot/meshcore-analyzer';
const GH = 'https://github.com/Kpa-clawbot/corescope';
test('returns empty string when all args missing', () => {
const { formatVersionBadge } = makeBadgeSandbox('');