Files
meshcore-analyzer/docs/api-spec.md
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

52 KiB
Raw Blame History

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.

Version: 1.0.0 Last updated: 2025-07-17


Table of Contents


Conventions

Types

Notation Meaning
string JSON string
number JSON number (integer or float)
boolean true / false
string (ISO) ISO 8601 timestamp, e.g. "2025-07-17T04:23:01.000Z"
string (hex) Hex-encoded bytes, uppercase, e.g. "4F01A3..."
number | null May be null when data is unavailable
[T] JSON array of type T; always [] when empty, never null
object Nested JSON object (shape defined inline)

Null Rules

  • Fields marked | null may be absent or null.
  • Array fields MUST be [] when empty, NEVER null.
  • String fields that are "unknown" SHOULD be null, not "".

Pagination

Paginated endpoints accept limit (default 50) and offset (default 0) as query params. They return total (the unfiltered/filtered count before pagination).

Error Responses

{ "error": "string" }
  • 400 — Bad request (missing/invalid params)
  • 404 — Resource not found

GET /api/stats

Server-wide statistics. Lightweight, cached 10s.

Response 200

{
  "totalPackets":        number,       // observation count (legacy name)
  "totalTransmissions":  number | null, // unique transmission count
  "totalObservations":   number,       // total observation records
  "totalNodes":          number,       // active nodes (last 7 days)
  "totalNodesAllTime":   number,       // all nodes ever seen
  "totalObservers":      number,       // observer device count
  "packetsLastHour":     number,       // observations in last hour
  "engine":              "node",       // backend engine identifier
  "version":             string,       // package.json version, e.g. "2.6.0"
  "commit":              string,       // git short SHA or "unknown"
  "counts": {
    "repeaters":         number,       // active repeaters (last 7 days)
    "rooms":             number,
    "companions":        number,
    "sensors":           number
  }
}

GET /api/health

Server health and telemetry. Used by monitoring.

Response 200

{
  "status":    "ok",
  "engine":    "node",
  "version":   string,
  "commit":    string,
  "uptime":    number,          // seconds
  "uptimeHuman": string,       // e.g. "4h 32m"
  "memory": {
    "rss":       number,       // MB
    "heapUsed":  number,       // MB
    "heapTotal": number,       // MB
    "external":  number        // MB
  },
  "eventLoop": {
    "currentLagMs": number,
    "maxLagMs":     number,
    "p50Ms":        number,
    "p95Ms":        number,
    "p99Ms":        number
  },
  "cache": {
    "entries":    number,
    "hits":       number,
    "misses":     number,
    "staleHits":  number,
    "recomputes": number,
    "hitRate":    number        // percentage (0100)
  },
  "websocket": {
    "clients":   number        // connected WS clients
  },
  "packetStore": {
    "packets":      number,    // loaded transmissions
    "estimatedMB":  number
  },
  "perf": {
    "totalRequests": number,
    "avgMs":         number,
    "slowQueries":   number,
    "recentSlow": [            // last 5
      {
        "path":   string,
        "ms":     number,
        "time":   string,      // ISO timestamp
        "status": number       // HTTP status
      }
    ]
  }
}

GET /api/perf

Detailed performance metrics per endpoint.

Response 200

{
  "uptime":        number,          // seconds since perf stats reset
  "totalRequests": number,
  "avgMs":         number,
  "endpoints": {
    "/api/packets": {               // keyed by route path
      "count":  number,
      "avgMs":  number,
      "p50Ms":  number,
      "p95Ms":  number,
      "maxMs":  number
    }
    // ... more endpoints
  },
  "slowQueries": [                  // last 20 queries > 100ms
    {
      "path":   string,
      "ms":     number,
      "time":   string,             // ISO timestamp
      "status": number
    }
  ],
  "cache": {
    "size":       number,
    "hits":       number,
    "misses":     number,
    "staleHits":  number,
    "recomputes": number,
    "hitRate":    number             // percentage (0100)
  },
  "packetStore": {                  // from PacketStore.getStats()
    "totalLoaded":       number,
    "totalObservations": number,
    "evicted":           number,
    "inserts":           number,
    "queries":           number,
    "inMemory":          number,
    "sqliteOnly":        boolean,
    "maxPackets":        number,
    "estimatedMB":       number,
    "maxMB":             number,
    "indexes": {
      "byHash":            number,
      "byObserver":        number,
      "byNode":            number,
      "advertByObserver":  number
    }
  },
  "sqlite": {
    "dbSizeMB":    number,
    "walSizeMB":   number,
    "freelistMB":  number,
    "walPages":    { "total": number, "checkpointed": number, "busy": number } | null,
    "rows": {
      "transmissions": number,
      "observations":  number,
      "nodes":         number,
      "observers":     number
    }
  },
  "goRuntime": {                    // Go server only
    "heapMB":       number,         // heap allocation in MB
    "sysMB":        number,         // total system memory in MB
    "numGoroutine": number,         // active goroutines
    "numGC":        number,         // completed GC cycles
    "gcPauseMs":    number          // last GC pause in ms
  }
}

POST /api/perf/reset

Resets performance counters. Requires API key.

Headers

  • X-API-Key: <key> (required if config.apiKey is set)

Response 200

{ "ok": true }

GET /api/nodes

Paginated node list with filtering.

Query Parameters

Param Type Default Description
limit number 50 Page size
offset number 0 Pagination offset
role string Filter by role: repeater, room, companion, sensor
region string Comma-separated IATA codes for regional filtering
lastHeard string Recency filter: 1h, 6h, 24h, 7d, 30d
sortBy string lastSeen Sort key: name, lastSeen, packetCount
search string Substring match on name
before string ISO timestamp; only nodes with first_seen <= before

Response 200

{
  "nodes": [
    {
      "public_key":    string,           // 64-char hex public key
      "name":          string | null,
      "role":          string,           // "repeater" | "room" | "companion" | "sensor"
      "lat":           number | null,
      "lon":           number | null,
      "last_seen":     string (ISO),
      "first_seen":    string (ISO),
      "advert_count":  number,
      "hash_size":     number | null,    // latest hash size (13 bytes)
      "hash_size_inconsistent": boolean, // true if flip-flopping
      "hash_sizes_seen": [number] | undefined, // present only if >1 unique size seen
      "last_heard":    string (ISO) | undefined // from in-memory packets or path relay
    }
  ],
  "total":  number,                      // total matching count (before pagination)
  "counts": {
    "repeaters":  number,                // global counts (not filtered by current query)
    "rooms":      number,
    "companions": number,
    "sensors":    number
  }
}

Notes:

  • hash_sizes_seen is only present when more than one hash size has been observed.
  • last_heard is only present when in-memory data provides a more recent timestamp than last_seen.

GET /api/nodes/search

Quick node search for autocomplete/typeahead.

Query Parameters

Param Type Required Description
q string yes Search term (name substring or pubkey prefix)

Response 200

{
  "nodes": [
    {
      "public_key":   string,
      "name":         string | null,
      "role":         string,
      "lat":          number | null,
      "lon":          number | null,
      "last_seen":    string (ISO),
      "first_seen":   string (ISO),
      "advert_count": number
    }
  ]
}

Returns { "nodes": [] } when q is empty.


GET /api/nodes/bulk-health

Bulk health summary for all nodes. Used by analytics dashboard.

Query Parameters

Param Type Default Description
limit number 50 Max nodes (capped at 200)
region string Comma-separated IATA codes for regional filtering

Response 200

Returns a JSON array (not wrapped in an object):

[
  {
    "public_key": string,
    "name":       string | null,
    "role":       string,
    "lat":        number | null,
    "lon":        number | null,
    "stats": {
      "totalTransmissions": number,
      "totalObservations":  number,
      "totalPackets":       number,   // same as totalTransmissions (backward compat)
      "packetsToday":       number,
      "avgSnr":             number | null,
      "lastHeard":          string (ISO) | null
    },
    "observers": [
      {
        "observer_id":   string,
        "observer_name": string | null,
        "avgSnr":        number | null,
        "avgRssi":       number | null,
        "packetCount":   number
      }
    ]
  }
]

Note: This is a bare array, not { nodes: [...] }.


GET /api/nodes/network-status

Aggregate network health status counts.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "total":      number,
  "active":     number,    // within degradedMs threshold
  "degraded":   number,    // between degradedMs and silentMs
  "silent":     number,    // beyond silentMs
  "roleCounts": {
    "repeater":  number,
    "room":      number,
    "companion": number,
    "sensor":    number
    // may include "unknown" if role is missing
  }
}

GET /api/nodes/:pubkey

Node detail page data.

Path Parameters

Param Type Description
pubkey string Node public key (hex)

Response 200

{
  "node": {
    "public_key":    string,
    "name":          string | null,
    "role":          string,
    "lat":           number | null,
    "lon":           number | null,
    "last_seen":     string (ISO),
    "first_seen":    string (ISO),
    "advert_count":  number,
    "hash_size":     number | null,
    "hash_size_inconsistent": boolean,
    "hash_sizes_seen": [number] | undefined
  },
  "recentAdverts": [Packet]   // last 20 packets for this node, newest first
}

Where Packet is a transmission object (see Packet Object).

Response 404

{ "error": "Not found" }

GET /api/nodes/:pubkey/health

Detailed health information for a single node.

Response 200

{
  "node": {                          // full node row
    "public_key":   string,
    "name":         string | null,
    "role":         string,
    "lat":          number | null,
    "lon":          number | null,
    "last_seen":    string (ISO),
    "first_seen":   string (ISO),
    "advert_count": number
  },
  "observers": [
    {
      "observer_id":   string,
      "observer_name": string | null,
      "packetCount":   number,
      "avgSnr":        number | null,
      "avgRssi":       number | null,
      "iata":          string | null
    }
  ],
  "stats": {
    "totalTransmissions": number,
    "totalObservations":  number,
    "totalPackets":       number,    // same as totalTransmissions (backward compat)
    "packetsToday":       number,
    "avgSnr":             number | null,
    "avgHops":            number,    // rounded integer
    "lastHeard":          string (ISO) | null
  },
  "recentPackets": [                 // last 20 packets, observations stripped
    {
      // Packet fields (see Packet Object) minus `observations`
      "observation_count": number    // added for display
    }
  ]
}

Response 404

{ "error": "Not found" }

GET /api/nodes/:pubkey/paths

Path analysis for a node — all paths containing this node's prefix.

Response 200

{
  "node": {
    "public_key": string,
    "name":       string | null,
    "lat":        number | null,
    "lon":        number | null
  },
  "paths": [
    {
      "hops": [
        {
          "prefix": string,        // raw hex hop prefix
          "name":   string,        // resolved node name
          "pubkey": string | null,
          "lat":    number | null,
          "lon":    number | null
        }
      ],
      "count":      number,        // times this path was seen
      "lastSeen":   string (ISO) | null,
      "sampleHash": string         // hash of a sample packet using this path
    }
  ],
  "totalPaths":         number,    // unique path signatures
  "totalTransmissions": number     // total transmissions with this node in path
}

Response 404

{ "error": "Not found" }

GET /api/nodes/:pubkey/analytics

Per-node analytics over a time range.

Query Parameters

Param Type Default Description
days number 7 Lookback window (1365)

Response 200

{
  "node": {                          // full node row (same shape as nodes table)
    "public_key": string, "name": string | null, "role": string,
    "lat": number | null, "lon": number | null,
    "last_seen": string (ISO), "first_seen": string (ISO), "advert_count": number
  },
  "timeRange": {
    "from": string (ISO),
    "to":   string (ISO),
    "days": number
  },
  "activityTimeline": [
    { "bucket": string (ISO),  "count": number }   // hourly buckets
  ],
  "snrTrend": [
    {
      "timestamp":     string (ISO),
      "snr":           number,
      "rssi":          number | null,
      "observer_id":   string | null,
      "observer_name": string | null
    }
  ],
  "packetTypeBreakdown": [
    { "payload_type": number, "count": number }
  ],
  "observerCoverage": [
    {
      "observer_id":   string,
      "observer_name": string | null,
      "packetCount":   number,
      "avgSnr":        number | null,
      "avgRssi":       number | null,
      "firstSeen":     string (ISO),
      "lastSeen":      string (ISO)
    }
  ],
  "hopDistribution": [
    { "hops": string, "count": number }    // "0", "1", "2", "3", "4+"
  ],
  "peerInteractions": [
    {
      "peer_key":    string,
      "peer_name":   string,
      "messageCount": number,
      "lastContact": string (ISO)
    }
  ],
  "uptimeHeatmap": [
    { "dayOfWeek": number, "hour": number, "count": number }  // 0=Sun, 023
  ],
  "computedStats": {
    "availabilityPct":    number,     // 0100
    "longestSilenceMs":   number,
    "longestSilenceStart": string (ISO) | null,
    "signalGrade":        string,     // "A", "A-", "B+", "B", "C", "D"
    "snrMean":            number,
    "snrStdDev":          number,
    "relayPct":           number,     // % of packets with >1 hop
    "totalPackets":       number,
    "uniqueObservers":    number,
    "uniquePeers":        number,
    "avgPacketsPerDay":   number
  }
}

Response 404

{ "error": "Not found" }

GET /api/packets

Paginated packet (transmission) list with filtering.

Query Parameters

Param Type Default Description
limit number 50 Page size
offset number 0 Pagination offset
type string Filter by payload type (number or name)
route string Filter by route type
region string Filter by region (IATA code substring)
observer string Filter by observer ID
hash string Filter by packet hash
since string ISO timestamp lower bound
until string ISO timestamp upper bound
node string Filter by node pubkey
nodes string Comma-separated pubkeys (multi-node filter)
order string DESC Sort direction: asc or desc
groupByHash string Set to "true" for grouped response
expand string Set to "observations" to include observation arrays

Response 200 (default)

{
  "packets": [Packet],    // see Packet Object below (observations stripped unless expand=observations)
  "total":   number,
  "limit":   number,
  "offset":  number
}

Response 200 (groupByHash=true)

{
  "packets": [
    {
      "hash":              string,
      "first_seen":        string (ISO),
      "count":             number,       // observation count
      "observer_count":    number,       // unique observers
      "latest":            string (ISO),
      "observer_id":       string | null,
      "observer_name":     string | null,
      "path_json":         string | null,
      "payload_type":      number,
      "route_type":        number,
      "raw_hex":           string (hex),
      "decoded_json":      string | null,
      "observation_count": number,
      "snr":               number | null,
      "rssi":              number | null
    }
  ],
  "total": number
}

Response 200 (nodes=... multi-node)

{
  "packets": [Packet],
  "total":   number,
  "limit":   number,
  "offset":  number
}

GET /api/packets/timestamps

Lightweight endpoint returning only timestamps for timeline sparklines.

Query Parameters

Param Type Required Description
since string yes ISO timestamp lower bound

Response 200

Returns a JSON array of timestamps (strings or numbers):

["2025-07-17T00:00:01.000Z", "2025-07-17T00:00:02.000Z", ...]

Response 400

{ "error": "since required" }

GET /api/packets/:id

Single packet detail with byte breakdown and observations.

Path Parameters

Param Type Description
id string Packet ID (numeric) or 16-char hex hash

Response 200

{
  "packet": Packet,                  // full packet/transmission object
  "path":   [string],                // parsed path hops (from packet.paths or [])
  "breakdown": {                     // byte-level packet structure
    "ranges": [
      {
        "start":  number,            // byte offset
        "end":    number,
        "label":  string,
        "hex":    string,
        "value":  string | number | null
      }
    ]
  } | null,
  "observation_count": number,
  "observations": [
    {
      "id":              number,
      "transmission_id": number,
      "hash":            string,
      "observer_id":     string | null,
      "observer_name":   string | null,
      "direction":       string | null,
      "snr":             number | null,
      "rssi":            number | null,
      "score":           number | null,
      "path_json":       string | null,
      "timestamp":       string (ISO),
      "raw_hex":         string (hex),
      "payload_type":    number,
      "decoded_json":    string | null,
      "route_type":      number
    }
  ]
}

Response 404

{ "error": "Not found" }

POST /api/packets

Ingest a raw packet. Requires API key.

Headers

  • X-API-Key: <key> (required if config.apiKey is set)

Request Body

{
  "hex":      string,        // required — raw hex-encoded packet
  "observer": string | null, // observer ID
  "snr":      number | null,
  "rssi":     number | null,
  "region":   string | null, // IATA code
  "hash":     string | null  // pre-computed content hash
}

Response 200

{
  "id":      number,         // packet/observation ID
  "decoded": {               // full decode result
    "header":  DecodedHeader,
    "path":    DecodedPath,
    "payload": object
  }
}

Response 400

{ "error": "hex is required" }

POST /api/decode

Decode a raw packet without storing it.

Request Body

{
  "hex": string              // required — raw hex-encoded packet
}

Response 200

{
  "decoded": {
    "header":  DecodedHeader,
    "path":    DecodedPath,
    "payload": object
  }
}

Response 400

{ "error": "hex is required" }

GET /api/observers

List all observers with packet counts.

Response 200

{
  "observers": [
    {
      "id":              string,
      "name":            string | null,
      "iata":            string | null,      // region code
      "last_seen":       string (ISO),
      "first_seen":      string (ISO),
      "packet_count":    number,
      "model":           string | null,      // hardware model
      "firmware":        string | null,
      "client_version":  string | null,
      "radio":           string | null,
      "battery_mv":      number | null,      // millivolts
      "uptime_secs":     number | null,
      "noise_floor":     number | null,      // dBm
      "packetsLastHour": number,             // computed, not from DB
      "lat":             number | null,      // from matched node
      "lon":             number | null,      // from matched node
      "nodeRole":        string | null       // from matched node
    }
  ],
  "server_time": string (ISO)                // server's current time
}

GET /api/observers/:id

Single observer detail.

Response 200

{
  "id":              string,
  "name":            string | null,
  "iata":            string | null,
  "last_seen":       string (ISO),
  "first_seen":      string (ISO),
  "packet_count":    number,
  "model":           string | null,
  "firmware":        string | null,
  "client_version":  string | null,
  "radio":           string | null,
  "battery_mv":      number | null,
  "uptime_secs":     number | null,
  "noise_floor":     number | null,
  "packetsLastHour": number
}

Response 404

{ "error": "Observer not found" }

GET /api/observers/:id/analytics

Per-observer analytics.

Query Parameters

Param Type Default Description
days number 7 Lookback window

Response 200

{
  "timeline": [
    { "label": string, "count": number }    // bucketed by hours/days
  ],
  "packetTypes": {
    "4": number,                             // keyed by payload_type number
    "5": number
  },
  "nodesTimeline": [
    { "label": string, "count": number }    // unique nodes per time bucket
  ],
  "snrDistribution": [
    { "range": string, "count": number }    // e.g. "6 to 8"
  ],
  "recentPackets": [Packet]                 // last 20 enriched observations
}

GET /api/channels

List decoded channels with message counts.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "channels": [
    {
      "hash":         string,        // channel name (used as key)
      "name":         string,        // decoded channel name
      "lastMessage":  string | null, // text of most recent message
      "lastSender":   string | null, // sender of most recent message
      "messageCount": number,
      "lastActivity": string (ISO)
    }
  ]
}

GET /api/channels/:hash/messages

Messages for a specific channel.

Path Parameters

Param Type Description
hash string Channel name (from /api/channels)

Query Parameters

Param Type Default Description
limit number 100 Page size
offset number 0 Pagination offset (from end)

Response 200

{
  "messages": [
    {
      "sender":           string,
      "text":             string,
      "timestamp":        string (ISO),
      "sender_timestamp": number | null,    // device timestamp (unreliable)
      "packetId":         number,
      "packetHash":       string,
      "repeats":          number,           // dedup count
      "observers":        [string],         // observer names
      "hops":             number,
      "snr":              number | null
    }
  ],
  "total": number                           // total deduplicated messages
}

GET /api/analytics/rf

RF signal analytics.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "totalPackets":       number,      // observations with SNR data
  "totalAllPackets":    number,      // all regional observations
  "totalTransmissions": number,      // unique transmission hashes
  "snr": {
    "min":    number,
    "max":    number,
    "avg":    number,
    "median": number,
    "stddev": number
  },
  "rssi": {
    "min":    number,
    "max":    number,
    "avg":    number,
    "median": number,
    "stddev": number
  },
  "snrValues":  Histogram,           // pre-computed histogram (20 bins)
  "rssiValues": Histogram,           // pre-computed histogram (20 bins)
  "packetSizes": Histogram,          // pre-computed histogram (25 bins)
  "minPacketSize": number,           // bytes
  "maxPacketSize": number,
  "avgPacketSize": number,
  "packetsPerHour": [
    { "hour": string, "count": number }   // "2025-07-17T04"
  ],
  "payloadTypes": [
    { "type": number, "name": string, "count": number }
  ],
  "snrByType": [
    { "name": string, "count": number, "avg": number, "min": number, "max": number }
  ],
  "signalOverTime": [
    { "hour": string, "count": number, "avgSnr": number }
  ],
  "scatterData": [
    { "snr": number, "rssi": number }    // max 500 points
  ],
  "timeSpanHours": number
}

Histogram Shape

{
  "bins": [
    { "x": number, "w": number, "count": number }
  ],
  "min": number,
  "max": number
}

GET /api/analytics/topology

Network topology analytics.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "uniqueNodes": number,
  "avgHops":     number,
  "medianHops":  number,
  "maxHops":     number,
  "hopDistribution": [
    { "hops": number, "count": number }      // capped at 25
  ],
  "topRepeaters": [
    {
      "hop":    string,         // raw hex prefix
      "count":  number,
      "name":   string | null,  // resolved name
      "pubkey": string | null
    }
  ],
  "topPairs": [
    {
      "hopA":    string,
      "hopB":    string,
      "count":   number,
      "nameA":   string | null,
      "nameB":   string | null,
      "pubkeyA": string | null,
      "pubkeyB": string | null
    }
  ],
  "hopsVsSnr": [
    { "hops": number, "count": number, "avgSnr": number }
  ],
  "observers": [
    { "id": string, "name": string }
  ],
  "perObserverReach": {
    "<observer_id>": {
      "observer_name": string,
      "rings": [
        {
          "hops": number,
          "nodes": [
            {
              "hop":       string,
              "name":      string | null,
              "pubkey":    string | null,
              "count":     number,
              "distRange": string | null   // e.g. "1-3" or null if constant
            }
          ]
        }
      ]
    }
  },
  "multiObsNodes": [
    {
      "hop":    string,
      "name":   string | null,
      "pubkey": string | null,
      "observers": [
        {
          "observer_id":   string,
          "observer_name": string,
          "minDist":       number,
          "count":         number
        }
      ]
    }
  ],
  "bestPathList": [
    {
      "hop":           string,
      "name":          string | null,
      "pubkey":        string | null,
      "minDist":       number,
      "observer_id":   string,
      "observer_name": string
    }
  ]
}

GET /api/analytics/channels

Channel analytics.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "activeChannels": number,
  "decryptable":    number,
  "channels": [
    {
      "hash":       string,
      "name":       string,
      "messages":   number,
      "senders":    number,        // unique sender count
      "lastActivity": string (ISO),
      "encrypted":  boolean
    }
  ],
  "topSenders": [
    { "name": string, "count": number }
  ],
  "channelTimeline": [
    { "hour": string, "channel": string, "count": number }
  ],
  "msgLengths": [number]            // raw array of message character lengths
}

GET /api/analytics/distance

Hop distance analytics.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "summary": {
    "totalHops":  number,
    "totalPaths": number,
    "avgDist":    number,      // km, 2 decimal places
    "maxDist":    number       // km
  },
  "topHops": [
    {
      "fromName": string,
      "fromPk":   string,
      "toName":   string,
      "toPk":     string,
      "dist":     number,      // km
      "type":     string,      // "R↔R" | "C↔R" | "C↔C"
      "snr":      number | null,
      "hash":     string,
      "timestamp": string (ISO)
    }
  ],
  "topPaths": [
    {
      "hash":      string,
      "totalDist": number,     // km
      "hopCount":  number,
      "timestamp": string (ISO),
      "hops": [
        {
          "fromName": string,
          "fromPk":   string,
          "toName":   string,
          "toPk":     string,
          "dist":     number
        }
      ]
    }
  ],
  "catStats": {
    "R↔R": { "count": number, "avg": number, "median": number, "min": number, "max": number },
    "C↔R": { "count": number, "avg": number, "median": number, "min": number, "max": number },
    "C↔C": { "count": number, "avg": number, "median": number, "min": number, "max": number }
  },
  "distHistogram": Histogram | [],   // empty array if no data
  "distOverTime": [
    { "hour": string, "avg": number, "count": number }
  ]
}

GET /api/analytics/hash-sizes

Hash size analysis across the network.

Query Parameters

Param Type Default Description
region string Comma-separated IATA codes

Response 200

{
  "total": number,              // packets analyzed
  "distribution": {
    "1": number,                // 1-byte hash count
    "2": number,                // 2-byte hash count
    "3": number                 // 3-byte hash count
  },
  "hourly": [
    { "hour": string, "1": number, "2": number, "3": number }
  ],
  "topHops": [
    {
      "hex":    string,         // raw hop hex
      "size":   number,         // bytes (ceil(hex.length/2))
      "count":  number,
      "name":   string | null,
      "pubkey": string | null
    }
  ],
  "multiByteNodes": [
    {
      "name":     string,
      "hashSize": number,
      "packets":  number,
      "lastSeen": string (ISO),
      "pubkey":   string | null
    }
  ]
}

GET /api/analytics/subpaths

Subpath frequency analysis.

Query Parameters

Param Type Default Description
minLen number 2 Minimum subpath length (≥2)
maxLen number 8 Maximum subpath length
limit number 100 Max results
region string Comma-separated IATA codes

Response 200

{
  "subpaths": [
    {
      "path":    string,        // "Node A → Node B → Node C"
      "rawHops": [string],      // ["aa", "bb", "cc"]
      "count":   number,
      "hops":    number,        // length of subpath
      "pct":     number         // percentage of totalPaths (0100)
    }
  ],
  "totalPaths": number
}

GET /api/analytics/subpath-detail

Detailed stats for a specific subpath.

Query Parameters

Param Type Required Description
hops string yes Comma-separated raw hex hop prefixes

Response 200

{
  "hops":  [string],                     // input hops echoed back
  "nodes": [
    {
      "hop":    string,
      "name":   string,
      "lat":    number | null,
      "lon":    number | null,
      "pubkey": string | null
    }
  ],
  "totalMatches": number,
  "firstSeen":    string (ISO) | null,
  "lastSeen":     string (ISO) | null,
  "signal": {
    "avgSnr":  number | null,
    "avgRssi": number | null,
    "samples": number
  },
  "hourDistribution": [number],         // 24-element array (index = UTC hour)
  "parentPaths": [
    { "path": string, "count": number }
  ],
  "observers": [
    { "name": string, "count": number }
  ]
}

GET /api/resolve-hops

Resolve path hop hex prefixes to node names with regional disambiguation.

Query Parameters

Param Type Required Description
hops string yes Comma-separated hex hop prefixes
observer string no Observer ID for regional context
originLat number no Origin latitude for distance-based disambiguation
originLon number no Origin longitude

Response 200

{
  "resolved": {
    "<hop>": {
      "name":         string | null,
      "pubkey":       string | null,
      "ambiguous":    boolean | undefined,   // true if multiple candidates
      "unreliable":   boolean | undefined,   // true if failed sanity check
      "candidates":   [Candidate],
      "conflicts":    [Candidate],
      "globalFallback": boolean | undefined,
      "filterMethod": string | undefined,    // "geo" | "observer"
      "hopBytes":     number | undefined,    // for ambiguous entries
      "totalGlobal":  number | undefined,
      "totalRegional": number | undefined,
      "filterMethods": [string] | undefined
    }
  },
  "region": string | null
}

Candidate shape:

{
  "name":         string,
  "pubkey":       string,
  "lat":          number | null,
  "lon":          number | null,
  "regional":     boolean,
  "filterMethod": string,
  "distKm":       number | null
}

GET /api/traces/:hash

All observations of a specific packet hash, sorted chronologically.

Path Parameters

Param Type Description
hash string Packet hash

Response 200

{
  "traces": [
    {
      "observer":      string | null,   // observer_id
      "observer_name": string | null,
      "time":          string (ISO),
      "snr":           number | null,
      "rssi":          number | null,
      "path_json":     string | null
    }
  ]
}

GET /api/config/theme

Theme and branding configuration (merged from config.json + theme.json).

Response 200

{
  "branding": {
    "siteName": string,          // default: "CoreScope"
    "tagline":  string           // default: "Real-time MeshCore LoRa mesh network analyzer"
    // ... additional branding keys from config/theme files
  },
  "theme": {
    "accent":      string,       // hex color, default "#4a9eff"
    "accentHover": string,
    "navBg":       string,
    "navBg2":      string
    // ... additional theme CSS values
  },
  "themeDark": {
    // dark mode overrides (may be empty object)
  },
  "nodeColors": {
    "repeater":  string,         // hex color
    "companion": string,
    "room":      string,
    "sensor":    string,
    "observer":  string
  },
  "typeColors": {
    // payload type → hex color overrides
  },
  "home": object | null          // home page customization
}

GET /api/config/regions

Available regions (IATA codes) merged from config + DB.

Response 200

{
  "<iata_code>": string          // code → display name
  // e.g. "SFO": "San Francisco", "LAX": "Los Angeles"
}

Returns a flat key-value object.


GET /api/config/client

Client-side configuration values.

Response 200

{
  "roles":              object | null,
  "healthThresholds":   object | null,
  "tiles":              object | null,
  "snrThresholds":      object | null,
  "distThresholds":     object | null,
  "maxHopDist":         number | null,
  "limits":             object | null,
  "perfSlowMs":         number | null,
  "wsReconnectMs":      number | null,
  "cacheInvalidateMs":  number | null,
  "externalUrls":       object | null,
  "propagationBufferMs": number          // default: 5000
}

GET /api/config/cache

Cache TTL configuration (raw values in seconds).

Response 200

Returns the raw cacheTTL object from config.json, or {} if not set:

{
  "stats":                number | undefined,    // seconds
  "nodeDetail":           number | undefined,
  "nodeHealth":           number | undefined,
  "nodeList":             number | undefined,
  "bulkHealth":           number | undefined,
  "networkStatus":        number | undefined,
  "observers":            number | undefined,
  "channels":             number | undefined,
  "channelMessages":      number | undefined,
  "analyticsRF":          number | undefined,
  "analyticsTopology":    number | undefined,
  "analyticsChannels":    number | undefined,
  "analyticsHashSizes":   number | undefined,
  "analyticsSubpaths":    number | undefined,
  "analyticsSubpathDetail": number | undefined,
  "nodeAnalytics":        number | undefined,
  "nodeSearch":           number | undefined,
  "invalidationDebounce": number | undefined
}

GET /api/config/map

Map default center and zoom.

Response 200

{
  "center": [number, number],      // [lat, lon], default [37.45, -122.0]
  "zoom":   number                 // default 9
}

GET /api/iata-coords

IATA airport/region coordinates for client-side regional filtering.

Response 200

{
  "coords": {
    "<iata_code>": {
      "lat": number,
      "lon": number,
      "radiusKm": number
    }
  }
}

GET /api/audio-lab/buckets

Representative packets bucketed by payload type for audio lab.

Response 200

{
  "buckets": {
    "<type_name>": [
      {
        "hash":              string,
        "raw_hex":           string (hex),
        "decoded_json":      string | null,
        "observation_count": number,
        "payload_type":      number,
        "path_json":         string | null,
        "observer_id":       string | null,
        "timestamp":         string (ISO)
      }
    ]
  }
}

WebSocket Messages

Connection

Connect to ws://<host> (or wss://<host> for HTTPS). No authentication. The server broadcasts messages to all connected clients.

Message Wrapper

All WebSocket messages use this envelope:

{
  "type": string,     // "packet" or "message"
  "data": object      // payload (shape depends on type)
}

Message Type: "packet"

Broadcast on every new packet ingestion.

{
  "type": "packet",
  "data": {
    "id":                number,           // observation or transmission ID
    "raw":               string (hex) | null,
    "decoded": {
      "header": {
        "routeType":       number,
        "payloadType":     number,
        "payloadVersion":  number,
        "payloadTypeName": string          // "ADVERT", "GRP_TXT", "TXT_MSG", etc.
      },
      "path": {
        "hops":            [string]        // hex hop prefixes
      },
      "payload":           object          // decoded payload (varies by type)
    },
    "snr":               number | null,
    "rssi":              number | null,
    "hash":              string | null,
    "observer":          string | null,    // observer_id
    "observer_name":     string | null,
    "path_json":         string | null,    // JSON-stringified hops array
    "packet":            Packet | undefined, // full packet object (when available)
    "observation_count": number | undefined
  }
}

Notes:

  • data.decoded is always present with at least header.payloadTypeName.
  • data.packet is included for raw packet ingestion (Format 1 / MQTT), may be absent for companion bridge messages.
  • data.path_json is the JSON-stringified version of data.decoded.path.hops.

Fields consumed by frontend pages:

Field live.js packets.js app.js channels.js
data.id
data.hash
data.raw
data.decoded.header.payloadTypeName
data.decoded.payload
data.decoded.path.hops
data.snr
data.rssi
data.observer
data.observer_name
data.packet
data.observation_count
data.path_json
(any) ✓ (*)

(*) app.js passes all messages to registered wsListeners and uses them only for cache invalidation.

Message Type: "message"

Broadcast for GRP_TXT (channel message) packets only. Same data shape as "packet" type. channels.js listens for this type to update the channel message feed in real time.

{
  "type": "message",
  "data": {
    // identical shape to "packet" data
  }
}

Shared Object Shapes

Packet Object

A transmission/packet as stored in memory and returned by most endpoints:

{
  "id":                number,              // transmission ID
  "raw_hex":           string (hex) | null,
  "hash":              string,              // content hash (dedup key)
  "first_seen":        string (ISO),        // when first observed
  "timestamp":         string (ISO),        // display timestamp (= first_seen)
  "route_type":        number,              // 0=DIRECT, 1=FLOOD, 2=reserved, 3=TRANSPORT
  "payload_type":      number,              // 0=REQ, 1=RESPONSE, 2=TXT_MSG, 3=ACK, 4=ADVERT, 5=GRP_TXT, 7=ANON_REQ, 8=PATH, 9=TRACE, 11=CONTROL
  "payload_version":   number | null,
  "decoded_json":      string | null,       // JSON-stringified decoded payload
  "observation_count": number,
  "observer_id":       string | null,       // from "best" observation
  "observer_name":     string | null,
  "snr":               number | null,
  "rssi":              number | null,
  "path_json":         string | null,       // JSON-stringified hop array
  "direction":         string | null,
  "score":             number | null,
  "observations":      [Observation] | undefined  // stripped by default on list endpoints
}

Observation Object

A single observation of a transmission by an observer:

{
  "id":              number,
  "transmission_id": number,
  "hash":            string,
  "observer_id":     string | null,
  "observer_name":   string | null,
  "direction":       string | null,
  "snr":             number | null,
  "rssi":            number | null,
  "score":           number | null,
  "path_json":       string | null,
  "timestamp":       string (ISO) | number,  // ISO string or unix epoch
  // Enriched fields (from parent transmission):
  "raw_hex":         string (hex) | null,
  "payload_type":    number,
  "decoded_json":    string | null,
  "route_type":      number
}

DecodedHeader

{
  "routeType":       number,
  "payloadType":     number,
  "payloadVersion":  number,
  "payloadTypeName": string    // human-readable name
}

DecodedPath

{
  "hops":      [string],       // hex hop prefixes, e.g. ["a1b2", "c3d4"]
  "hashSize":  number,         // bytes per hop hash (13)
  "hashCount": number          // number of hops in path field
}

Payload Type Reference

Value Name Description
0 REQ Request
1 RESPONSE Response
2 TXT_MSG Direct text message
3 ACK Acknowledgement
4 ADVERT Node advertisement
5 GRP_TXT Group/channel text message
7 ANON_REQ Anonymous request
8 PATH Path / traceroute
9 TRACE Trace response
11 CONTROL Control message

Route Type Reference

Value Name Description
0 DIRECT Direct (with transport codes)
1 FLOOD Flood/broadcast
2 (reserved)
3 TRANSPORT Transport (with transport codes)