API-Referenz · Stand Juli 2026

CoreScope API – die sprechende Doku

CoreScope ist ein Echtzeit-Analyzer für MeshCore-LoRa-Netze: Er sammelt Pakete von Observern (per MQTT und HTTP), dekodiert sie, verfolgt Nodes und liefert Analytics über Topologie, Funkqualität und Kanäle. Diese Seite beschreibt alle HTTP-Endpoints des Go-Servers plus den WebSocket-Feed – was sie tun, welche Parameter sie nehmen und was zurückkommt.

Basis-URL: http://<host>:<port>/api/… Format: JSON (UTF-8) 76 Endpoints in 10 Gruppen Live-Spec am Server: GET /api/spec · Swagger UI: GET /api/docs

Grundlagen – wie die API tickt

Alle Endpoints liegen unter /api/ und antworten mit JSON. Fehler kommen einheitlich als {"error": "…"} mit passendem Statuscode: 400 für ungültige Parameter, 404 für unbekannte Ressourcen, 500 für Server-/DB-Fehler und 503, wenn der Server noch lädt oder ein Index kalt ist.

Lesen ist frei, Schreiben braucht einen Key

Alle lesenden Endpoints sind ohne Authentifizierung nutzbar. Schreibende und administrative Endpoints (in dieser Doku mit 🔑 X-API-Key markiert) verlangen den Header X-API-Key, geprüft in konstanter Zeit gegen apiKey aus der config.json. Drei Ablehnungsfälle: kein Key am Server konfiguriert → 403 („write endpoints disabled“), falscher Key → 401, als schwach erkannter Key → 403.

# Beispiel: Statistiken holen
curl -s http://localhost:8080/api/stats | jq

# Beispiel: Paket ingesten (Schreib-Endpoint)
curl -s -X POST http://localhost:8080/api/packets \
  -H "X-API-Key: $CORESCOPE_KEY" -H "Content-Type: application/json" \
  -d '{"hex":"11048F…","observer":"obs-01","snr":8.5,"rssi":-92}'

Caching & CDN

Jede /api/*-Antwort trägt Cache-Control: no-store – CDNs und Browser dürfen nichts zwischenspeichern. Einzige Ausnahme ist /api/known-channels (öffentlich cachebar, 30–300 s). Unabhängig davon cachen viele Handler serverseitig (z. B. Stats 10 s, Scope-Stats 30 s, Reach 5 min); wer schneller pollt, bekommt identische Antworten. Läuft CoreScope hinter Cloudflare & Co., braucht die Zone eine Bypass-Regel für /api/* (siehe docs/deployment-behind-cdn.md).

Pagination & Zeitfenster

Listen-Endpoints nehmen limit (Default 50) und offset (Default 0) und liefern total mit – die Anzahl vor der Pagination. Analytics-Endpoints akzeptieren wahlweise window (1h, 24h, 7d, …) oder absolute Grenzen from/to (RFC 3339); absolute Grenzen gewinnen.

region vs. area – zwei verschiedene Geo-Filter

?region= filtert Observer-basiert über IATA-Codes (kommasepariert, z. B. SFO,LAX): Es zählt, wo empfangen wurde. ?area= filtert Sender-basiert über die GPS-Position des Nodes, wie sie in seinen ADVERT-Paketen steht – der Key kommt aus config.json (areas, Polygon oder Bounding-Box). Weil nur ADVERTs den Absender preisgeben, fließen bei aktivem area-Filter nur ADVERT-Pakete in die Auswertung ein. Details unten in der Referenz.

System & Monitoring

Alles, was man an ein Monitoring hängt oder beim Debuggen zuerst aufruft: Health, Zähler, Performance-Zahlen und der Zustand der MQTT-Quellen.

GET/api/health

Der große Gesundheitscheck: Status, Version/Commit/Build, Uptime, Speicher (RSS, Heap), GC-Pausen-Perzentile (als eventLoop-Äquivalent), Cache-Statistik, Anzahl verbundener WebSocket-Clients, PacketStore-Füllstand und eine Perf-Kurzfassung mit den letzten fünf Slow-Queries.

Antwort: status, engine, version, commit, uptime/uptimeHuman, memory{}, eventLoop{}, cache{}, websocket.clients, packetStore{}, perf{}.

GET/api/healthz

Readiness-Probe für Orchestrierung und Load-Balancer. Antwortet 503 mit {"ready":false,"reason":"loading"}, solange die Hintergrund-Initialisierung läuft (Store-Load, Best-Observation-Auswahl, Neighbor-Graph), danach 200.

Antwort (ready): ready, loadedTx, loadedObs, from_pubkey_backfill{total,processed,done}, optional ingest_liveness (je MQTT-Quelle lastReceiptUnix/lastMessageUnix).

Hinweis: ingest_liveness stammt aus dem Ingestor-Stats-File und ist ~1 s memoisiert.

GET/api/stats

Netzweite Zähler für Dashboard-Kacheln: Pakete, Transmissions, Observations, Nodes (aktiv in 7 Tagen und all-time), Observer, Pakete der letzten Stunde/24 h sowie Rollen-Counts (Repeater, Rooms, Companions, Sensors).

Antwort: u. a. totalPackets, totalTransmissions, totalObservations, totalNodes, totalNodesAllTime, totalObservers, packetsLastHour, packetsLast24h, counts{}, signatureDrops, hashMigrationComplete und der Speicher-Breakdown storeDataMB/processRSSMB/goHeapInuseMB/goSysMB.

Cache: serverseitig 10 s.

GET/api/scope-stats

Statistik über Transport-Scopes (Code1-Feld transportgerouteter Pakete): Wie viel Verkehr ist scoped vs. unscoped, welche Regionen sind aktiv, und wie entwickelt sich das über die Zeit?

ParamDefaultBeschreibung
window24h1h, 24h oder 7d; bestimmt auch die Bucket-Größe der Zeitreihe (5 min / 1 h / 6 h)

Antwort: summary{transportTotal, scoped, unscoped, unknownScope}, byRegion[], timeSeries[].

Fehler: 400 bei ungültigem window; 500, wenn die scope_name-Migration des Ingestors noch nicht gelaufen ist. Cache: 30 s pro Fenster. Auf Bestandsdaten ist unscoped bis zum Abschluss des Startup-Backfills überhöht.

GET/api/perf

Detaillierte Performance-Sicht: pro Endpoint Count, Avg, p50/p95, Max; die letzten 20 Slow-Queries (> 100 ms); Cache-, PacketStore- und SQLite-Statistiken sowie Go-Runtime-Daten (Goroutines, GC, Heap).

ParamDefaultBeschreibung
mem1 schaltet den teuren Store-Speicher-Breakdown zu (O(tx+obs)-Walk, nur auf Anfrage)

Antwort: uptime, totalRequests, avgMs, endpoints{}, slowQueries[], cache{}, packetStore{}, sqlite{}, goRuntime{}, optional memoryBreakdown{}.

GET/api/perf/io

Disk-I/O-Raten des Server-Prozesses pro Sekunde, berechnet als Delta von /proc/self/io seit dem letzten Aufruf – der erste Aufruf liefert daher Nullen. Wenn verfügbar, hängt ein gleichförmiges Sample des Ingestors mit dran.

Antwort: readBytesPerSec, writeBytesPerSec, cancelledWriteBytesPerSec, syscallsRead, syscallsWrite, optional ingestor{}.

Hinweis: Das Ingestor-Sample wird verworfen, wenn es älter als 5 s ist. Fehlerfrei by design.

GET/api/perf/sqlite

SQLite-Sicht: WAL-Größe, Page-Metriken und eine Cache-Hit-Rate (Proxy über den PacketStore-Row-Cache, kein nativer SQLite-Zähler).

Antwort: walSizeMB, walSize, pageCount, pageSize, cacheSize, cacheHitRate.

Hinweis: Nullwerte bei :memory:-DB oder fehlender DB – kein Fehler.

GET/api/perf/write-sources

Write-Pfad-Zähler des Ingestors aus dessen Stats-File: Inserts, Duplikate, Upserts, Fehler, WAL-Commits, Backfill-Fortschritt – plus Latenz-Histogramme des SQLite-Writer-Locks.

Antwort: sources{tx_inserted, tx_dupes, obs_inserted, node_upserts, observer_upserts, write_errors, sig_drops, walCommits, groupCommitFlushes, backfill_*}, sampleAt, writer_perf{}.

Hinweis: Fail-soft – fehlt das Stats-File, kommt eine leere Antwort mit 200.

GET/api/mqtt/status

Zustand jeder konfigurierten MQTT-Quelle: verbunden oder nicht, letzte Verbindung/Trennung, letztes Paket, Zähler gesamt und der letzten 5 Minuten. Passwörter in Broker-URLs werden maskiert (user:****@host) – auch in Fehlermeldungen.

Antwort: sources[{name, broker, connected, lastConnectUnix, lastDisconnectUnix, lastPacketUnix, connectCount, disconnectCount, packetsTotal, packetsLast5m, lastError?}], sampleAt, optional watchdogLastTickUnix/watchdogPanicCount.

Hinweis: Fail-soft – ohne Ingestor-Stats-File eine leere Liste, kein Fehler.

POST/api/perf/reset 🔑 X-API-Key

Setzt alle Performance-Zähler zurück (Requests, Endpoint-Statistiken, Slow-Queries). Praktisch vor einem Lasttest oder nach einem Deployment.

Antwort: {"ok": true}

Admin & Betrieb

Alles hier verlangt den X-API-Key-Header. Schreibende Aktionen laufen bewusst über den Ingestor (der das schreibbare DB-Handle hält) – der Server enqueued nur.

PUT/api/config/geo-filter 🔑 X-API-Key

Speichert das Geo-Filter-Polygon dauerhaft in die config.json (atomar via Temp-Datei) und tauscht die In-Memory-Konfiguration sofort. Ein leeres Polygon löscht den Filter.

Body-FeldRegeln
polygon[[lat,lon],…] – leer oder 3–1000 Punkte; lat ∈ [−90,90], lon ∈ [−180,180], kein NaN/Inf
bufferKmendlich, 0–20 000

Antwort: gespeicherte Konfiguration als Echo – {polygon, bufferKm}.

Fehler: 400 bei Validierungsfehlern (Body-Limit 1 MB), 500 wenn das Speichern scheitert. Parallele PUTs werden serialisiert.

POST/api/admin/prune-geo-filter 🔑 X-API-Key

Räumt Nodes auf, deren GPS außerhalb des Geo-Filters liegt – in zwei Stufen. Ohne Parameter ein Dry-Run, der nur auflistet. Mit ?confirm=true und einem Body {"pubkeys":[…]} (muss Pubkeys aus dem Preview echoen – TOCTOU-Schutz) wird ein Marker-File enqueued; das eigentliche DELETE führt der Ingestor aus.

Antwort: Dry-Run → {dryRun:true, count, nodes[]}; Confirm → 202 mit {accepted:true, requestId, count, nodes, statusUrl}.

Fehler: 400 ohne konfigurierten Geo-Filter oder bei Confirm ohne Pubkeys; 500 bei DB-/Enqueue-Fehlern.

GET/api/admin/prune-geo-filter/status 🔑 X-API-Key

Pollt den Zustand eines zuvor enqueuten Prune-Auftrags über dessen requestId.

ParamBeschreibung
idRequest-ID aus der Confirm-Antwort (Pflicht)

Antwort: {requestId, status:"pending"} oder {requestId, status:"done"|"error", deleted, requestedAt, completedAt, error}.

Fehler: 400 ohne/mit ungültiger ID, 404 bei unbekannter ID.

GET/api/debug/affinity 🔑 X-API-Key

Diagnose-Blick in den Neighbor-Graph: alle Affinity-Edges samt Gewicht, Score, Jaccard und SNR – plus die Entscheidungs-Logs der Prefix-Disambiguierung und Aggregat-Statistiken.

ParamBeschreibung
prefixoptional; nur Einträge zu diesem Hop-Prefix (case-insensitiv)
nodeoptional; nur Einträge, an denen dieser Node beteiligt ist

Antwort: edges[], resolutions[], stats{totalEdges, resolvedCount, ambiguousCount, unresolvedCount, avgConfidence, cacheAge, lastRebuild, …}.

GET/api/dropped-packets 🔑 X-API-Key

Zuletzt verworfene Pakete mit Verwerfungsgrund – zum Nachvollziehen, warum etwas nicht in der DB gelandet ist. Neueste zuerst.

ParamDefaultBeschreibung
limit100max. 500
observerFilter auf Observer-ID
pubkeyFilter auf Node-Pubkey

Antwort: nacktes Array mit {id, hash, raw_hex, reason, observer_id, observer_name, node_pubkey, node_name, dropped_at}.

GET/api/backup 🔑 X-API-Key

Streamt einen konsistenten, defragmentierten SQLite-Snapshot der Analyzer-DB (VACUUM INTO auf eine Temp-Datei – blockiert die Writes des Ingestors nicht).

Antwort: Binärdownload, application/octet-stream, Dateiname corescope-backup-<unix>.db, mit Content-Length.

Fehler: 503 ohne DB, 500 bei Snapshot-Fehlern. Bricht der Stream nach den Headern ab, entsteht ein verkürzter Download (nur geloggt).

Konfiguration

Read-only-Endpoints, mit denen sich das Frontend selbst konfiguriert: Themes, Regionen, Areas, Karten-Defaults und Client-Limits. Der einzige Schreibweg ist PUT /api/config/geo-filter (siehe Admin).

GET/api/config/client

Das Bündel an Laufzeit-Konfiguration für den Browser-Client: Rollen, Health-Schwellen, Karten-/Tile-Konfiguration, SNR-/Distanz-Schwellen, Limits, Reconnect-Zeiten und Feature-Flags.

Antwort: u. a. roles, healthThresholds, map/tiles/mapDarkTileProvider, snrThresholds, distThresholds, maxHopDist, limits, perfSlowMs, wsReconnectMs, cacheInvalidateMs, externalUrls, propagationBufferMs, liveMapMaxNodes, timestamps, customizer.disabledTabs (immer [], nie null) und das Opt-in-Flag clientRxCoverage.

GET/api/config/theme

Branding und Farbwelt, gemerged aus eingebauten Defaults, config.json und theme.json: helles und dunkles Theme, Node-Rollen-Farben, Payload-Typ-Farben und die Home-Page-Konfiguration.

Antwort: branding{siteName, tagline, …}, theme{}, themeDark{}, nodeColors{}, typeColors{}, home{}, markerStroke{}.

Kompatibilität: Der Payload-Typ-Key REQ wird zusätzlich als Legacy-Alias REQUEST emittiert; eingehende Configs mit REQUEST werden normalisiert.

GET/api/config/regions

Alle bekannten Regionen als flaches Objekt IATA-Code → Anzeigename – die konfigurierten Regionen vereinigt mit allen Codes, die tatsächlich in der DB vorkommen.

Antwort: z. B. {"SFO":"San Francisco","LAX":"LAX"}.

GET/api/config/areas

Die in config.json definierten Area-Filter als leichte Liste für die Filter-Leiste im Frontend – ohne Geometrie. Einträge ohne Label (Kommentar-Keys) fehlen.

Antwort: [{key, label}], [] wenn keine Areas konfiguriert sind.

GET/api/config/areas/polygons

Dieselben Areas, aber mit voller Geometrie (Polygon-Punkte bzw. Bounding-Box) – gedacht für Karten-/Debug-Werkzeuge.

Antwort: [{key, label, polygon?, latMin?, latMax?, lonMin?, lonMax?}].

GET/api/config/map

Karten-Startposition.

Antwort: {center:[lat,lon], zoom} – Default [37.45, -122.0], Zoom 9.

GET/api/config/cache

Das rohe cacheTTL-Objekt aus der config.json (Sekundenwerte je Client-Cache), oder {} wenn nicht gesetzt.

GET/api/config/geo-filter

Aktueller Geo-Filter. writeEnabled verrät dem Customizer-UI, ob am Server ein starker API-Key konfiguriert ist – also ob der PUT-Weg überhaupt offen ist.

Antwort: {polygon, bufferKm, writeEnabled}; ohne Filter polygon:null, bufferKm:0.

GET/api/iata-coords

Koordinaten und Radius je IATA-Code – Grundlage für die Regional-Filterung und Observer-Positionierung im Client.

Antwort: {coords: {"SFO": {lat, lon, radiusKm}, …}}.

Pakete & Dekodierung

Das Herz der API. Begriffsklärung: Eine Transmission ist ein eindeutiges Paket (per Content-Hash dedupliziert); eine Observation ist die Sichtung dieser Transmission durch einen Observer – eine Transmission kann viele Observations haben.

GET/api/packets

Die paginierte Paketliste mit dem vollen Filterbesteck. Standardmäßig kommen Transmissions ohne ihre Observation-Arrays (schlanker); expand=observations liefert sie mit.

ParamDefaultBeschreibung
limit / offset50 / 0Pagination
typePayload-Typ (Zahl, siehe Referenz)
routeRoute-Typ (Zahl)
observerObserver-ID
hashPaket-Hash
since / untilISO-Zeitgrenzen
region / areaGeo-Filter (siehe Grundlagen)
nodeein Node-Pubkey
nodeskommaseparierte Pubkeys (Multi-Node-Filter)
channelChannel-Filter
orderdescasc oder desc
groupByHashtrue → nach Hash gruppierte Antwortform mit observer_count
expandobservations → Observation-Arrays inkludieren

Antwort: {packets[], total, limit, offset}.

GET/api/packets/{id}

Paket-Detail – per numerischer ID oder 16-stelligem Hex-Hash. Ist das Paket schon aus dem In-Memory-Store gealtert, greift automatisch der SQLite-Fallback, damit Links von Node-Detailseiten nicht ins Leere laufen.

Antwort: {packet, path[], observation_count, observations[]} – inklusive Byte-Breakdown der Paketstruktur, wo verfügbar.

Fehler: 404, wenn weder Store noch DB das Paket kennen.

GET/api/packets/timestamps

Nur die Zeitstempel seit einem Startpunkt – das Leichtgewicht für Timeline-Sparklines.

ParamBeschreibung
sinceISO-Zeitstempel, Pflicht – sonst 400

Antwort: nacktes Array von Zeitstempeln.

POST/api/packets 🔑 X-API-Key

Ingest-Endpoint: nimmt ein rohes Hex-Paket an, dekodiert es und schreibt Transmission + Observation in die DB (v3-Schema; der Observer wird bei Bedarf angelegt).

Body-FeldBeschreibung
hexrohes Paket, hex-codiert – Pflicht
observerObserver-ID (optional; wird per Upsert angelegt)
snr / rssiEmpfangswerte (optional)
region / hashoptional

Antwort: {id, decoded:{header, path, payload}}.

Fehler: 400 bei fehlendem/undekodierbarem Hex, 500 bei DB-Fehlern. Bei TRACE-Paketen kommt path_json aus den Payload-Hops, nicht aus den Header-Bytes (dort stehen SNR-Werte).

POST/api/packets/observations

Batch-Abfrage: Observations für bis zu 200 Hashes in einem Request – spart dem Frontend viele Einzel-Roundtrips. Trotz POST ein lesender Endpoint, daher ohne API-Key.

Request: {"hashes":["abc…","def…"]} · Antwort: {"results": {"abc…": [Observations], …}}.

Fehler: 400 bei ungültigem JSON oder > 200 Hashes; leere Liste → leeres Ergebnis.

POST/api/decode

Dekodiert ein Hex-Paket, ohne irgendetwas zu speichern – der „Was ist das für ein Paket?“- Endpoint für Werkzeuge und Debugging.

Request: {"hex":"…"} · Antwort: {decoded:{header, path, payload}}.

Fehler: 400 bei leerem oder undekodierbarem Hex.

GET/api/traces/{hash}

Alle Sichtungen eines Paket-Hashes in chronologischer Reihenfolge – wer hat wann mit welchem SNR gehört? Grundlage der Ausbreitungs-Ansicht.

Antwort: {traces:[{observer, observer_name, time, snr, rssi, path_json}]}.

Nodes

Alles über die Mesh-Teilnehmer: Listen, Suche, Health, Pfade, Analytics, Batterie, Nachbarn und Uhren-Drift. Per-Node-Endpoints antworten für blacklisted oder versteckte Nodes mit 404 – Anfragende erfahren nicht, ob die Row existiert.

GET/api/nodes

Die paginierte Node-Liste. Repeater- und Room-Rows tragen zusätzlich die Usefulness-Metriken (Issue #672): die vier Achsen traffic_share_score, bridge_score, coverage_score, redundancy_score (je 0–1), den Komposit-Score usefulness_score mit Note usefulness_grade (A–F) sowie Relay-Aktivität: relay_active, relay_count_1h, relay_count_24h, unscoped_relay_count_24h und last_relayed. Ein hoher unscoped_relay_count_24h deutet auf ein Basis-Konfigurationsproblem des Repeaters hin (flood.max.unscoped nicht 0).

ParamDefaultBeschreibung
limit / offset50 / 0Pagination
rolerepeater, room, companion, sensor
region / areaGeo-Filter
lastHeardRecency: 1h, 6h, 24h, 7d, 30d
sortBylastSeenname, lastSeen, packetCount
searchSubstring-Match auf den Namen
beforenur Nodes mit first_seen ≤ before

Antwort: {nodes[], total, counts{repeaters, rooms, companions, sensors}}counts ist global, nicht vom Filter beeinflusst. Node-Rows enthalten außerdem hash_size/hash_size_inconsistent/hash_sizes_seen und default_scope.

GET/api/nodes/resolve

Löst einen gehörten Pfad-Prefix (oder ganzen Pubkey) zu Name und Pubkey auf, ohne die ganze Node-Liste laden zu müssen.

ParamBeschreibung
prefixPflicht; Hex, mindestens 4 Zeichen (2-Zeichen-Prefixe sind bewusst gesperrt, um Enumeration zu erschweren)

Antwort: {prefix, pubkey?, name?, ambiguous} – genau ein Treffer füllt pubkey/name; mehrere Treffer → ambiguous:true; blacklisted/hidden wird wie „nicht gefunden“ behandelt.

Fehler: 400 bei Nicht-Hex oder zu kurzem Prefix; 503 ohne DB.

GET/api/nodes/bulk-health

Health-Zusammenfassung für viele Nodes in einem Call – fürs Analytics-Dashboard.

ParamDefaultBeschreibung
limit50max. 200
regionIATA-Codes, kommasepariert

Antwort: nacktes Array (nicht in {nodes:…} gewrappt) mit {public_key, name, role, lat, lon, stats{…}, observers[]}.

GET/api/nodes/network-status

Aggregierter Netz-Zustand: Wie viele Nodes sind frisch, angeschlagen oder still? Die Grenzen kommen aus den konfigurierten Health-Schwellen (degraded/silent).

Antwort: {total, active, degraded, silent, roleCounts{}}; Filter region wird unterstützt.

GET/api/nodes/{pubkey}

Detailseite eines Nodes: die volle Node-Row (inkl. Usefulness-Achsen und relay_window_hours bei Repeatern/Rooms) plus die letzten 20 Transmissions als recentAdverts.

Antwort: {node, recentAdverts[]}.

Fehler: 404 bei unbekanntem, blacklisted oder verstecktem Node.

GET/api/nodes/{pubkey}/health

Health-Detail eines Nodes: Welche Observer hören ihn (mit SNR/RSSI und Paketzahl je Observer), Gesamt-Stats und die letzten 20 Pakete.

Antwort: {node, observers[], stats{totalTransmissions, totalObservations, packetsToday, avgSnr, avgHops, lastHeard}, recentPackets[]}.

GET/api/nodes/{pubkey}/paths

Alle beobachteten Pfade, in denen der Prefix dieses Nodes vorkommt – mit aufgelösten Hops, Häufigkeit und Beispiel-Hash je Pfad-Signatur.

Antwort: {node, paths:[{hops[], count, lastSeen, sampleHash}], totalPaths, totalTransmissions}.

GET/api/nodes/{pubkey}/analytics

Die Analytics-Seite eines Nodes über ein wählbares Fenster: Aktivitäts-Timeline (Stunden-Buckets), SNR-Trend, Payload-Typ-Verteilung, Observer-Coverage, Hop-Verteilung, Peer-Interaktionen, Uptime-Heatmap – plus berechnete Kennzahlen wie availabilityPct, signalGrade (A–D) und relayPct.

ParamDefaultBeschreibung
days7Lookback 1–365

Antwort: {node, timeRange, activityTimeline[], snrTrend[], packetTypeBreakdown[], observerCoverage[], hopDistribution[], peerInteractions[], uptimeHeatmap[], computedStats{}}.

GET/api/nodes/{pubkey}/battery

Batteriespannungs-Zeitreihe eines Nodes plus Status-Ampel aus dem jüngsten Messwert gegen die konfigurierten mV-Schwellen.

ParamDefaultBeschreibung
days7gültig 1–365, sonst stiller Fallback auf 7

Antwort: {public_key, days, samples:[{timestamp, battery_mv}], latest_mv, latest_ts, status, thresholds{low_mv, critical_mv}}status ist ok, low (< 3300 mV Default), critical (< 3000 mV) oder unknown ohne Samples.

Fehler: 400 ohne Pubkey, 404 bei unbekanntem Node.

GET/api/nodes/{pubkey}/neighbors

Die First-Hop-Nachbarn aus dem Affinity-Graph: je Nachbar Score (Sättigung × Recency × Observer-Diversität), Beobachtungszahlen nach Hash-Prefix-Modus, SNR, Distanz und die beteiligten Observer. Mehrdeutige Kanten tragen Kandidaten-Listen.

ParamDefaultBeschreibung
min_count1Mindest-Beobachtungen je Kante
min_score0.0Mindest-Affinity-Score
include_ambiguoustruefalse blendet mehrdeutige Kanten aus

Antwort: {node, neighbors:[{pubkey, prefix, name, role, count, counts_by_mode, score, avg_snr, distance_km, observers[], ambiguous, unresolved, candidates[]}], total_observations}, sortiert nach Score absteigend.

Fehler: 404 bei blacklisted/hidden Node. Der Graph wird lazy gebaut und mit TTL gecacht.

GET/api/nodes/{pubkey}/reach

Der Zwei-Wege-Reichweiten-Report: Aus der Pfad-Adjazenz werden richtungsbehaftete Link-Zähler berechnet (we_hear/they_hear); ein Link ist bidirektional, wenn beide Richtungen belegt sind, und der bottleneck (die schwächere Richtung) bewertet die Stabilität. Nodes werden nur über eindeutige 2–3-Byte-Prefixe identifiziert – reliable_tokens: [] heißt: Node ist in Pfaden nicht zuverlässig identifizierbar, links bleibt leer.

ParamDefaultBeschreibung
days7Lookback, geklemmt auf 1–30

Antwort: {node, window, reliable_tokens[], importance{neighbor_degree, degree_rank, relay_observations, bidirectional_links, direct_observers}, direct_observers[], links[]}.

Cache/Limits: Antwort 5 min pro pubkey|days gecacht; Pfad-Scan hart bei 200 000 Zeilen gekappt (dann repräsentative Stichprobe). Fehler: 400 wenn pubkey keine 64 Hex-Zeichen ist, 404 bei unbekannt/blacklisted.

GET/api/nodes/clock-skew

Uhren-Drift der ganzen Flotte, berechnet aus ADVERT-Zeitstempel-Paaren: je Node der geschätzte Offset, die Drift-Rate und eine Severity-Einstufung.

ParamBeschreibung
areaoptional; ohne area kommt die Antwort aus dem 30-s-Hintergrund-Snapshot, mit area wird live gerechnet

Antwort: nacktes Array [{pubkey, nodeName, nodeRole, skewMs, driftPerDaySec, severity, samples:null}] – die Detail-Felder (samples, Evidence) sind in der Fleet-Ansicht bewusst weggelassen.

GET/api/nodes/{pubkey}/clock-skew

Volle Clock-Skew-Details eines einzelnen Nodes – inklusive der Sample-Serie für Sparklines, Kalibrier-Zusammenfassung und Hash-Evidenz der letzten Auffälligkeiten.

Antwort: {pubkey, meanSkewSec, medianSkewSec, recentMedianSkewSec, driftPerDaySec, severity, sampleCount, calibrated, samples[], goodFraction, recentBadSamples[], recentHashEvidence, calibrationSummary}.

Fehler: 404, wenn (noch) keine Skew-Daten für den Node existieren.

Observer

Observer sind die Empfangsstationen (Gateways), die Pakete an CoreScope melden – mit eigener Hardware-Telemetrie: Firmware, Batterie, Noise-Floor, Uptime.

GET/api/observers

Alle Observer mit Paketzählern und Geräte-Telemetrie; Position und Rolle kommen vom gematchten Node, sofern zuordenbar.

Antwort: {observers:[{id, name, iata, last_seen, first_seen, packet_count, model, firmware, client_version, radio, battery_mv, uptime_secs, noise_floor, packetsLastHour, lat, lon, nodeRole}], server_time}.

Cache: die ungefilterte Standardantwort kommt aus einem Snapshot-Cache mit Singleflight-Refill (kein Thundering-Herd auf die DB).

GET/api/observers/{id}

Detail eines einzelnen Observers – dieselben Felder wie in der Liste.

Fehler: 404 („Observer not found“).

GET/api/observers/{id}/metrics

Zeitreihen der Gerätemetriken eines Observers (Batterie, Noise-Floor, …) plus erkannte Reboot-Zeitpunkte.

ParamDefaultBeschreibung
sincejetzt − 24 hRFC-3339-Startzeit
untiloptionales Ende
resolution5m5m, 1h oder 1d – alles andere → 400

Antwort: {observer_id, observer_name, reboots[], metrics[]}.

Achtung: Eine unbekannte ID gibt kein 404, sondern 200 mit leeren Arrays.

GET/api/observers/{id}/analytics

Analytics je Observer: Paket-Timeline, Payload-Typ-Verteilung, Anzahl eindeutiger Nodes je Zeit-Bucket, SNR-Verteilung und die letzten 20 angereicherten Pakete.

ParamDefaultBeschreibung
days7Lookback-Fenster

Antwort: {timeline[], packetTypes{}, nodesTimeline[], snrDistribution[], recentPackets[]}.

GET/api/observers/metrics/summary

Aggregierte Metriken über alle Observer in einem Fenster – die Flotten-Übersicht für den Health-Vergleich.

ParamDefaultBeschreibung
window24hGo-Duration (24h, 72h, …) oder Tages-Suffix (7d)
regionIATA-Filter, case-insensitiv

Antwort: {observers:[MetricsSummaryRow]}.

Fehler: 400 bei unparsbarem window.

GET/api/observers/clock-skew

Die berechneten Uhren-Offsets je Observer (Kalibrierung), mit denen die Node-Skew-Werte korrigiert werden. Positiver Offset = Observer-Uhr geht vor.

Antwort: Array [{observerID, offsetSec, samples}], sortiert nach |Offset| absteigend.

Channels

Gruppen-Chat im Mesh: dekodierbare Channels, ihre Messages (dedupliziert über alle Observer) und der Community-Katalog bekannter Hashtag-Channels.

GET/api/channels

Alle dekodierten Channels mit letzter Nachricht, letztem Absender, Message-Zahl und letzter Aktivität.

ParamBeschreibung
regionIATA-Codes, kommasepariert (optional)

Antwort: {channels:[{hash, name, lastMessage, lastSender, messageCount, lastActivity}]}.

GET/api/channels/{hash}/messages

Die Messages eines Channels – dedupliziert: repeats zählt, wie oft dieselbe Nachricht gehört wurde, observers nennt die Zeugen.

ParamDefaultBeschreibung
limit100Seitengröße
offset0Offset vom Ende

Antwort: {messages:[{sender, text, timestamp, sender_timestamp, packetId, packetHash, repeats, observers[], hops, snr}], total}.

GET/api/known-channels

Der gecachte Community-Katalog bekannter Hashtag-Channels (Issue #1323) – fail-soft aus einem Atomic-Snapshot serviert, blockiert nie auf den Upstream-Fetch.

ParamBeschreibung
regionISO-3166-1-alpha-2-Code, case-insensitiv (optional)

Antwort: {generatedAt?, license?, fetchedAt, source, entries:[{channel, description?, key?, region, regionName?}]}.

Besonderheit: der einzige /api-Endpoint mit Browser-Caching – Cache-Control: public, max-age=300 (bzw. 30 s bei leerem Cache); Upstream wird alle 24 h neu geholt.

Analytics

Die Auswertungs-Endpoints hinter dem Analytics-Dashboard. Fast alle akzeptieren region und/oder area; viele werden von einem Hintergrund-Recomputer vorgerechnet, damit der erste Seitenaufruf schnell ist.

GET/api/analytics/roles

Rollenverteilung im Netz, angereichert mit der Clock-Skew-Haltung je Rolle: Wie viele Repeater laufen mit gesunder Uhr, wie viele driften kritisch?

Antwort: {totalNodes, roles:[{role, nodeCount, withSkew, meanAbsSkewSec, medianAbsSkewSec, okCount, warningCount, criticalCount, absurdCount, noClockCount}]}, sortiert nach Node-Zahl.

Cache: aus dem Analytics-Snapshot-Recomputer (Issue #1256).

GET/api/analytics/rf

Funk-Statistik: SNR/RSSI-Verteilungen (min/max/avg/median/stddev plus vorgerechnete Histogramme), Paketgrößen, Pakete pro Stunde, Payload-Typ-Verteilung, SNR nach Typ, Signal über Zeit und ein SNR/RSSI-Scatter (max. 500 Punkte).

ParamBeschreibung
regionIATA-Codes (optional)
areaArea-Key – beschränkt auf ADVERTs von Nodes in der Area

Antwort: {totalPackets, totalTransmissions, snr{}, rssi{}, snrValues, rssiValues, packetSizes, packetsPerHour[], payloadTypes[], snrByType[], signalOverTime[], scatterData[], timeSpanHours}.

GET/api/analytics/topology

Topologie-Sicht: Hop-Verteilung, die fleißigsten Repeater und Repeater-Paare, SNR nach Hop-Zahl, Reichweiten-Ringe je Observer und Nodes, die von mehreren Observern gehört werden.

ParamBeschreibung
region / areaGeo-Filter (bei area zählen nur Hops, die zu Nodes in der Area auflösen)

Antwort: {uniqueNodes, avgHops, medianHops, maxHops, hopDistribution[], topRepeaters[], topPairs[], hopsVsSnr[], observers[], perObserverReach{}, multiObsNodes[], bestPathList[]}.

GET/api/analytics/channels

Channel-Statistik: aktive und entschlüsselbare Channels, Top-Sender, Aktivitäts-Timeline je Channel und die Verteilung der Nachrichtenlängen.

Antwort: {activeChannels, decryptable, channels[], topSenders[], channelTimeline[], msgLengths[]}. Filter: region, area.

GET/api/analytics/distance

Hop-Distanzen in Kilometern: die weitesten Einzel-Hops und Gesamtpfade, Statistik nach Link-Kategorie (R↔R, C↔R, C↔C), Histogramm und Verlauf über die Zeit.

Antwort: {summary{totalHops, totalPaths, avgDist, maxDist}, topHops[], topPaths[], catStats{}, distHistogram, distOverTime[]}. Filter: region, area.

GET/api/analytics/hash-sizes

Verteilung der Hash-Prefix-Größen (1/2/3 Byte) im Netz – gesamt, stündlich, die häufigsten Hops und alle Nodes, die mit Multi-Byte-Hashes senden.

Antwort: {total, distribution{1,2,3}, hourly[], topHops[], multiByteNodes[]}. Filter: region, area.

GET/api/analytics/hash-collisions

Findet Hash-Prefixe, die sich mehrere Nodes teilen – die Ursache mehrdeutigen Routings. Firmware-seitig heißt das: 1-Byte-Hashes kollidieren schnell.

Antwort: {collisions:[{hash, count, nodes[]}], totalCollisions, affectedPackets}. Filter: region, area.

GET/api/analytics/subpaths

Häufigkeitsanalyse von Teilpfaden: Welche Hop-Sequenzen tauchen immer wieder auf? Zeigt die „Autobahnen“ des Meshes.

ParamDefaultBeschreibung
minLen / maxLen2 / 8Subpath-Längenbereich (min ≥ 2)
limit100max. Ergebnisse
regionIATA-Filter

Antwort: {subpaths:[{path, rawHops[], count, hops, pct}], totalPaths}.

GET/api/analytics/subpaths-bulk

Mehrere Subpath-Längen-Buckets in einem Request – spart wiederholte Scans, wenn das Dashboard z. B. 2er-, 3er- und 4er-Pfade gleichzeitig zeigt.

ParamBeschreibung
groupsPflicht; Format minLen-maxLen:limit,… – z. B. 2-2:50,3-3:30,5-8:15; Limit je Gruppe auf 200 geklemmt
regionoptional
window / from / toZeitfenster

Antwort: {results:[{subpaths, totalPaths}]} – in der Reihenfolge der Gruppen.

Achtung: Validierungsfehler kommen als 200 mit {error:"…"}-Body zurück, nicht als 4xx. 503, solange der Subpath-Index noch lädt.

GET/api/analytics/subpath-detail

Tiefenanalyse eines konkreten Teilpfads: aufgelöste Nodes, Trefferzahl, Zeitraum, Signalqualität, Stundenverteilung, Eltern-Pfade und beteiligte Observer.

ParamBeschreibung
hopsPflicht; kommaseparierte Hex-Hop-Prefixe, z. B. aa,bb,cc

Antwort: {hops[], nodes[], totalMatches, firstSeen, lastSeen, signal{avgSnr, avgRssi, samples}, hourDistribution[24], parentPaths[], observers[]}.

GET/api/analytics/neighbor-graph

Der komplette Nachbarschafts-Graph des Meshes für die Visualisierung: Nodes, gewichtete Kanten (mit Score, Bidirektionalität, SNR, Ambiguität) und Aggregat-Statistiken.

ParamDefaultBeschreibung
min_count5Mindest-Beobachtungen je Kante
min_score0.1Mindest-Score
regionObserver-Region
rolemind. ein Kanten-Endpunkt muss die Rolle haben

Antwort: {nodes:[{pubkey, name, role, neighbor_count}], edges:[{source, target, weight, score, bidirectional, avg_snr, ambiguous}], stats{total_nodes, total_edges, ambiguous_edges, …}}.

Cache: zwei Standard-Parameterformen werden alle 5 min im Hintergrund vorgerechnet und tragen dann den Header X-Cache-Age-Seconds; alle anderen Kombinationen rechnen live.

GET/api/analytics/relay-airtime-share

Wie viel Funk-Airtime verbraucht welcher Payload-Typ wirklich? Score = LoRa Time-on-Air × Anzahl distinkter Relays – macht sichtbar, welche Pakettypen das Netz durch Flood-Verstärkung am meisten belasten.

ParamBeschreibung
window1h, 24h/1d, 3d, 7d/1w, 30d; unbekannte Werte → gesamter Datenbestand
from / toabsolute RFC-3339-Grenzen, haben Vorrang

Antwort: {rows:[{payload_type, type, count, count_pct, score, airtime_pct}], total_count, total_score, preset{freq_hz, bw_khz, sf, cr, preamble}, window, cached}.

Cache: 60 s (konfigurierbar) je Fenster über den geteilten RF-Cache.

GET/api/audio-lab/buckets

Repräsentative Pakete je Payload-Typ, gebucketed fürs Audio-Lab (Paket-Sonifikation).

Antwort: {buckets: {"<typ>": [{hash, raw_hex, decoded_json, observation_count, payload_type, path_json, observer_id, timestamp}]}}.

Pfad-Tools & RX-Coverage

Werkzeuge rund um Pfad-Auflösung und die Mobile-Empfangs-Abdeckung. Die drei RX-Coverage-Endpoints sind opt-in: Ohne das Config-Flag clientRxCoverage antworten sie mit einem sauberen 404 (das Flag ist über /api/config/client abfragbar).

GET/api/resolve-hops

Löst eine Liste von Hop-Hex-Prefixen zu Node-Namen auf – mit regionaler Disambiguierung über den Observer-Kontext oder eine Ursprungs-Koordinate. Mehrdeutige Prefixe kommen mit Kandidaten- und Konflikt-Listen zurück.

ParamBeschreibung
hopsPflicht; kommaseparierte Hex-Prefixe
observerObserver-ID als regionaler Kontext (optional)
originLat / originLonUrsprungs-Koordinate für Distanz-Disambiguierung (optional)

Antwort: {resolved: {"<hop>": {name, pubkey, ambiguous?, unreliable?, candidates[], conflicts[], filterMethod?, …}}, region}.

POST/api/paths/inspect

Der Pfad-Inspektor (Issue #944): Eine Beam-Search bewertet, welche konkreten Node-Ketten hinter einer beobachteten Prefix-Sequenz stecken – mit Score, per-Hop-Evidenz und Alternativen je Position.

Body-FeldRegeln
prefixesPflicht; ≤ 64 Einträge, jeweils gültiges Hex mit gerader Länge, ≤ 3 Byte, alle gleich lang
contextoptional {observerId, since, until} (fließt in den Cache-Key)
limitDefault 10, max. 50

Antwort: {candidates:[{path[], names[], score, speculative, evidence.perHop[]}], input{prefixes, hops}, stats{beamWidth, expansionsRun, elapsedMs}, stale?}speculative markiert Scores unter 0.7.

Fehler/Cache: 400 bei Regelverstößen (Body-Limit 4 KB); 503 {"retry":true} beim Kaltstart (Graph wird asynchron gebaut). Ergebnisse 30 s gecacht; ein veralteter Graph wird mit stale:true serviert (stale-while-revalidate).

GET/api/rx-coverage Opt-in: clientRxCoverage

Globale Mobile-RX-Coverage als GeoJSON-Hexbin-Grid: Welche Zellen haben mobile Clients Empfang gemeldet, mit welchem besten SNR?

ParamDefaultBeschreibung
bboxPflicht: minLat,minLon,maxLat,maxLon
days7geklemmt 1–30
zLeaflet-Zoom → Hex-Auflösung, geklemmt 3–18
node / rxFilter auf einen gehörten Node bzw. einen Empfänger

Antwort: GeoJSON-FeatureCollection; je Zelle properties{cell, count, best_snr, has_sig, nodes[], nodes_truncated}, plus truncated (Cap: 5000 dichteste Zellen).

Fehler: 404 wenn Feature aus; 400 bei fehlender/ungültiger bbox; 503 ohne DB.

GET/api/nodes/{pubkey}/rx-coverage Opt-in: clientRxCoverage

RX-Coverage aus Sicht eines Nodes: In welchen Hex-Zellen wurde er gehört? Jede Zelle trägt einen Node-Breakdown (max. 25 Einträge), dazu kommen die node-weiten Zähler mobile_receptions und mobile_clients.

ParamBeschreibung
pubkeyPfad; muss 64 Hex-Zeichen sein → sonst 400
bboxPflicht: minLat,minLon,maxLat,maxLon
zZoom → Hex-Auflösung, geklemmt 3–18

Antwort: GeoJSON-FeatureCollection wie oben, plus mobile_receptions/mobile_clients.

Fehler: 404 bei Feature-aus oder blacklisted/hidden Node; 400/503 wie oben.

GET/api/rx-leaderboard Opt-in: clientRxCoverage

Rangliste der mobilen Observer nach Coverage-Score. Der Score gewichtet Zellen mit 1/N (N = wie viele Observer die Zelle abdecken) – wer seltene Gegenden abdeckt, punktet; wer dieselbe Zelle zuspammt, nicht.

ParamDefaultBeschreibung
days7geklemmt 1–30
limit20außerhalb 1–100 → zurück auf 20

Antwort: {days, observers:[{pubkey, name, receptions, nodes, cells, score}]}.

Limits: In-Memory-Scan bei 500 000 Zeilen gekappt (neueste zuerst) – dann ist das Ranking eine Teilmenge. 404 wenn Feature aus.

Meta & Live-Feed

GET/api/spec

Die maschinenlesbare OpenAPI-3.0-Spezifikation – zur Laufzeit aus dem Router generiert, daher immer synchron mit den tatsächlich registrierten Routen. Mit Access-Control-Allow-Origin: *, also auch von fremden Origins ladbar.

GET/api/docs

Interaktive Swagger UI über /api/spec. Lädt die Swagger-Assets von unpkg – braucht also Internetzugang im Browser.

WS/ws

Der Live-Feed: Der Server broadcastet jede Paket-Ingestion an alle verbundenen Clients – ohne Authentifizierung. Jede Message ist ein Envelope {type, data}. type:"packet" kommt bei jedem Paket; type:"message" zusätzlich (mit identischem data) für GRP_TXT-Channel-Nachrichten, damit Chat-Ansichten gezielt lauschen können.

data enthält: id, raw (Hex), decoded{header{routeType, payloadType, payloadTypeName}, path{hops[]}, payload}, snr, rssi, hash, observer, observer_name, path_json, optional packet und observation_count.

Hinweis: WS-Pushes und REST-GETs sind als Paar gedacht – der Feed invalidiert Client-Caches, die GETs liefern den konsistenten Zustand.

Referenz

Payload-Typen

WertNameBedeutung
0REQRequest
1RESPONSEResponse
2TXT_MSGDirekte Textnachricht
3ACKBestätigung
4ADVERTNode-Advertisement (trägt Pubkey + GPS)
5GRP_TXTGruppen-/Channel-Nachricht
7ANON_REQAnonymer Request
8PATHPath / Traceroute
9TRACETrace-Response
11CONTROLControl-Message

Route-Typen

WertNameBedeutung
0DIRECTDirekt (mit Transport-Codes)
1FLOODFlood/Broadcast
2reserviert
3TRANSPORTTransport (mit Transport-Codes)

Der Area-Filter im Detail

Areas werden in config.json unter areas definiert – entweder als Polygon ([[lat,lon],…], mindestens 3 Punkte, Ray-Casting-Test) oder als Bounding-Box (latMin/latMax/lonMin/lonMax). Attribution läuft über die GPS-Position aus den ADVERTs des Senders; da alle anderen Pakettypen verschlüsselte Absender haben, fließen bei aktivem ?area= nur ADVERT-Pakete ein. Die Node-Position ist so frisch wie das letzte ADVERT (bei Repeatern typisch 12–24 h); das Area-Node-Set wird serverseitig 30 s gecacht.

Antimeridian: Polygone über die Datumsgrenze (180°) werden nicht unterstützt – solche Gebiete in zwei Einträge aufteilen.

Versteckte & blacklistete Nodes

Betreiber können Nodes per Blacklist oder Namens-Prefix verstecken. Per-Node-Endpoints antworten dann mit 404, Auflösungs-Endpoints behandeln sie als „nicht gefunden“, und Ranglisten blenden sie aus – Anfragende können nicht unterscheiden, ob der Node nicht existiert oder verborgen ist.