{
  "components": {
    "schemas": {
      "CandidateEntry": {
        "description": "A candidate pubkey offered when a neighbor edge is ambiguous.",
        "properties": {
          "name": {
            "description": "Candidate node display name.",
            "type": "string"
          },
          "pubkey": {
            "description": "Candidate node public key (hex).",
            "type": "string"
          },
          "role": {
            "description": "Candidate node role (e.g. repeater, room).",
            "type": "string"
          }
        },
        "type": "object"
      },
      "NeighborEntry": {
        "description": "One neighbor of the queried node, with affinity score and observation metadata.",
        "properties": {
          "ambiguous": {
            "type": "boolean"
          },
          "avg_snr": {
            "nullable": true,
            "type": "number"
          },
          "candidates": {
            "items": {
              "$ref": "#/components/schemas/CandidateEntry"
            },
            "type": "array"
          },
          "count": {
            "description": "Total observations supporting this neighborship.",
            "type": "integer"
          },
          "counts_by_mode": {
            "additionalProperties": {
              "type": "integer"
            },
            "description": "#1638: observation counts keyed by hash-prefix mode in bytes (1/2/3; 0 = legacy/unknown).",
            "type": "object"
          },
          "distance_km": {
            "nullable": true,
            "type": "number"
          },
          "first_seen": {
            "type": "string"
          },
          "last_seen": {
            "type": "string"
          },
          "name": {
            "nullable": true,
            "type": "string"
          },
          "observers": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "prefix": {
            "description": "Raw hop hash prefix that established this edge.",
            "type": "string"
          },
          "pubkey": {
            "description": "Resolved neighbor public key, or null when only a hop prefix is known.",
            "nullable": true,
            "type": "string"
          },
          "role": {
            "nullable": true,
            "type": "string"
          },
          "score": {
            "description": "Affinity score: count saturation × recency decay × observer-diversity confidence.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "unresolved": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "Node": {
        "additionalProperties": true,
        "description": "A mesh node. Repeater and room nodes additionally carry the issue #672 usefulness metrics and relay-activity fields below; those fields are absent on other roles. NOTE: coverage_score, redundancy_score and usefulness_grade ship only with the #672 4-axis scorer (PR #1762) and are absent on every build without it; until that lands usefulness_score is aliased to traffic_share_score. Only traffic_share_score and bridge_score ship today.",
        "properties": {
          "advert_count": {
            "type": "integer"
          },
          "battery_mv": {
            "nullable": true,
            "type": "integer"
          },
          "bridge_score": {
            "description": "#672 Bridge axis: normalized betweenness centrality (chokepoint importance). Repeater/room only.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "coverage_score": {
            "description": "#672 Coverage axis: normalized harmonic reach centrality (how much of the mesh the node can reach). Repeater/room only.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "declared_regions": {
            "description": "Repeater/room only (#1862): named regions from this node's newest declared-regions answer, spelled as GET /api/scope-audit declaredRegions spells them (leading '#' stripped, '*' wildcard excluded). Empty array: it answered and named no region. Absent: it never answered, this database has no declared-regions source, or the declared-regions lookup failed. Absence is not evidence the node lacks a region.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "declared_regions_truncated": {
            "description": "Repeater/room only (#1862): present, and true, only when the answer behind declared_regions was flagged as truncated, so that list is partial. GET /api/scope-audit shows the same flag as truncated. Absent otherwise, including for a source that does not record truncation, so absence does not mean the list is complete.",
            "type": "boolean"
          },
          "first_seen": {
            "description": "RFC3339 timestamp of the first observation.",
            "type": "string"
          },
          "flood_advert_count_7d": {
            "description": "Distinct FLOOD adverts originated in the last 7 days (zero-hop adverts excluded). Present on the node detail endpoint.",
            "type": "integer"
          },
          "last_relayed": {
            "description": "Repeater/room only: RFC3339 time this node last appeared as a relay hop.",
            "type": "string"
          },
          "last_seen": {
            "description": "RFC3339 timestamp of the most recent observation.",
            "type": "string"
          },
          "lat": {
            "nullable": true,
            "type": "number"
          },
          "lon": {
            "nullable": true,
            "type": "number"
          },
          "name": {
            "description": "Node display name (most recent advert name).",
            "type": "string"
          },
          "public_key": {
            "description": "Node public key (hex).",
            "type": "string"
          },
          "redundancy_score": {
            "description": "#672 Redundancy axis: normalized articulation-point criticality — 1 means removing the node fragments the mesh, 0 means alternate paths exist. Repeater/room only.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "relay_active": {
            "description": "Repeater/room only: relayed traffic within the active window.",
            "type": "boolean"
          },
          "relay_count_1h": {
            "description": "Repeater/room only: relay-hop appearances in the last hour.",
            "type": "integer"
          },
          "relay_count_24h": {
            "description": "Repeater/room only: relay-hop appearances in the last 24 hours.",
            "type": "integer"
          },
          "relay_window_hours": {
            "description": "Repeater/room only, /api/nodes/{pubkey} detail endpoint only: width (hours) of the relay-activity window the relay_count_* values cover.",
            "type": "integer"
          },
          "role": {
            "description": "Node role (e.g. repeater, room, client, sensor).",
            "type": "string"
          },
          "temperature_c": {
            "nullable": true,
            "type": "number"
          },
          "traffic_share_score": {
            "description": "#672 Traffic axis: share of non-advert traffic relayed through this repeater. Repeater/room only.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "unscoped_relay_count_24h": {
            "description": "Repeater/room only: subset of relay_count_24h that were unscoped floods (route_type FLOOD). A well-configured repeater sets flood.max.unscoped 0, so a non-trivial count flags a base-config problem.",
            "type": "integer"
          },
          "usefulness_grade": {
            "description": "Letter grade derived from usefulness_score. Repeater/room only.",
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F"
            ],
            "type": "string"
          },
          "usefulness_score": {
            "description": "#672 composite usefulness = 0.30·bridge + 0.25·coverage + 0.25·redundancy + 0.20·traffic. Until the 4-axis scorer ships (PR #1762) this is aliased to traffic_share_score. Repeater/room only.",
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          }
        },
        "type": "object"
      },
      "NodeAdvert": {
        "additionalProperties": true,
        "description": "A recent transmission/advert from a node (the /api/packets transmission shape). Only the commonly-used fields are documented.",
        "properties": {
          "first_seen": {
            "description": "RFC3339 time the transmission was first observed.",
            "type": "string"
          },
          "from_pubkey": {
            "description": "Originating node public key.",
            "type": "string"
          },
          "hash": {
            "description": "Transmission content hash.",
            "type": "string"
          },
          "id": {
            "type": "integer"
          },
          "payload_type": {
            "description": "MeshCore payload type.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "NodeDetailResponse": {
        "properties": {
          "node": {
            "$ref": "#/components/schemas/Node"
          },
          "recentAdverts": {
            "description": "Up to 20 most recent transmissions from this node (newest first).",
            "items": {
              "$ref": "#/components/schemas/NodeAdvert"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "NodeListResponse": {
        "properties": {
          "counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Per-role node counts.",
            "type": "object"
          },
          "has_more": {
            "description": "True when rows exist past this page. Decided before the blacklist / hidden-prefix / geo-filter / area passes, which drop rows from the page and rewrite total — so neither the page length nor total can be used to stop paginating. Paginate until this is false.",
            "type": "boolean"
          },
          "nodes": {
            "items": {
              "$ref": "#/components/schemas/Node"
            },
            "type": "array"
          },
          "total": {
            "description": "Total nodes matching the query after filtering.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "NodeNeighborsResponse": {
        "properties": {
          "neighbors": {
            "items": {
              "$ref": "#/components/schemas/NeighborEntry"
            },
            "type": "array"
          },
          "node": {
            "description": "The queried node's public key.",
            "type": "string"
          },
          "total_observations": {
            "type": "integer"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "in": "header",
        "name": "X-API-Key",
        "type": "apiKey"
      },
      "CookieAuth": {
        "description": "User-management session (only when userManagement.enabled). Unsafe methods also need the X-CS-CSRF header from GET /api/auth/me.",
        "in": "cookie",
        "name": "cs_session",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "description": "MeshCore network analyzer — packet capture, node tracking, and mesh analytics.",
    "license": {
      "name": "MIT"
    },
    "title": "CoreScope API",
    "version": "v3.14.0-on8ar.7"
  },
  "openapi": "3.0.3",
  "paths": {
    "/api/account": {
      "delete": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Delete own account",
        "tags": [
          "users"
        ]
      },
      "patch": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Update profile",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/companions": {
      "get": {
        "description": "[{pubkey, name, linkedAt, lastSeenAt}], newest link first. lastSeenAt is the newest client reception of that companion (rx_at as stored) or null. Accepts a device token.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "List own linked companions",
        "tags": [
          "users"
        ]
      },
      "post": {
        "description": "Body {pubkey, challenge, signature, name}. signature is the companion's Ed25519 signature (64 bytes, hex) over the UTF-8 string \"corescope-link:\" + host of userManagement.publicBaseUrl + \":\" + challenge. The challenge is consumed in every case. 200 {pubkey, name, linkedAt}. The synced settings (meshcore-my-nodes) are not touched: a companion is not a node to monitor. A companion linked to another account moves to the caller (the newest proof wins); both get a companion.transfer audit row and the previous owner a mail (a security notice, sent whatever the node notification settings are). 400 malformed pubkey or signature, or a signature that does not verify; 410 challenge missing, expired, used, or bound to another user or pubkey; 429 rate limited. Accepts a device token.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Link a companion",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/companions/challenge": {
      "post": {
        "description": "Body {pubkey} (64 hex). 200 {challenge, expiresAt, host}: host is what to sign (the host of userManagement.publicBaseUrl); challenge is 32 random bytes as hex, single use, valid 5 minutes, bound to the caller and the pubkey. 400 malformed pubkey; 429 above 60 challenge + link requests per hour per user and per IP. Accepts a device token (Authorization: Bearer).",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Start linking a companion",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/companions/{pubkey}": {
      "delete": {
        "description": "204. Stored coverage stays. 400 malformed pubkey, 404 not linked to the caller. Accepts a device token.",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Unlink an own companion",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/confirm-email": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Confirm an address change",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/email": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Request an address change",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/export": {
      "get": {
        "description": "One JSON file (Content-Disposition attachment corescope-account-\u003cYYYY-MM-DD\u003e.json): {formatVersion: 1, exportedAt, instance, profile, sessions (with kind web|device and the device label), companions (linked companions: pubkey, name, linkedAt), settings, proposals, notifications: {prefs, watches}, audit, mail}. Every row, no caps. Leaves out the password hash, every session, link and unsubscribe token, and companion link challenges. Audit rows name other accounts by id only. Includes activatedAt, activatedBy (id) and each mail row's to-address. Records a user.export audit row after the file is built.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Download own account data",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/notifications": {
      "get": {
        "description": "Returns {enabled, events, availableEvents, externalLabel, watches: [{pubkey, name, known, createdAt}], limits: {maxWatches, perUserPerDay, mailsLast24h}}. node.external and externalLabel appear only when notifications.externalAlerts is configured. Creates the default settings (on, node.offline and node.battery) on first use. known is false once the node left the analyzer database. Registered only when userManagement.notifications.enabled.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Get own notification settings",
        "tags": [
          "users"
        ]
      },
      "put": {
        "description": "Body {enabled, events}. events from node.offline, node.battery, node.external (when externalAlerts is configured) and, for admins, foreign.new and observer.offline. Answers the GET body. 400 unknown event type or node.external without externalAlerts, 403 an admin event chosen by a non-admin. Events left out lose their stored state, so choosing them again starts without a mail.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Change own notification settings",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/notifications/watch-my-nodes": {
      "post": {
        "description": "Copies the pubkeys of the caller's synced meshcore-my-nodes setting into the watch list, up to the limit. Answers {added, already, skipped, account}; skipped counts malformed keys, nodes not in the analyzer database and nodes over the limit; account is the GET body.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Watch the synced My nodes",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/notifications/watches/{pubkey}": {
      "delete": {
        "description": "Answers the GET body; removing a node that is not watched is 200. 400 when pubkey is not 64 hex characters.",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Stop watching a node",
        "tags": [
          "users"
        ]
      },
      "put": {
        "description": "Answers the GET body. 400 when pubkey is not 64 hex characters, 404 when the node is not in the analyzer database, 409 when userManagement.notifications.maxWatchesPerUser nodes are watched. Watching a watched node is 200.",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Watch a node",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/password": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Change password",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/proposals": {
      "get": {
        "description": "The caller's proposals, newest first: [{id, kind, subject, status, note, createdAt, decidedAt}]. note is the reviewer's reason.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "List own proposals",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/sessions": {
      "get": {
        "description": "[{id, kind, label, createdAt, lastSeenAt, expiresAt, userAgent, current}]. kind is web (a browser) or device (a CoreDrive RX device token); label is the device name, empty for web sessions. DELETE /api/account/sessions/{id} revokes either kind.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "List own sessions",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/sessions/{id}": {
      "delete": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Revoke one own session",
        "tags": [
          "users"
        ]
      }
    },
    "/api/account/settings": {
      "delete": {
        "description": "Removes the stored document; the next PUT (baseRevision 0) starts a new one in a new generation.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Delete own synced settings",
        "tags": [
          "users"
        ]
      },
      "get": {
        "description": "Returns {revision, generation, doc, allowlist}. doc is {v: 1, keys: {\u003clocalStorage key\u003e: \u003craw string\u003e}} or null at revision 0. generation is a random id of the stored document (empty at revision 0); revisions restart at 1 after a DELETE, the generation does not repeat. allowlist lists the keys the server accepts: {key, kind: set|scalar, id?}, where id names the field that identifies an item of a set (absent: the item itself).",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Get own synced settings",
        "tags": [
          "users"
        ]
      },
      "put": {
        "description": "Body {baseRevision, baseGeneration, doc}. 200 {revision, generation}. 409 {revision, generation, doc} when baseRevision is not the stored revision or, with a stored document, baseGeneration is not its generation. baseRevision 0 without a stored document starts a new generation. 400 when doc is not {v: 1, keys} or holds a key that is never synced or not allowlisted. 413 above 256 KiB. 429 above 60 PUT requests per hour per user.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Replace own synced settings",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/audit": {
      "get": {
        "description": "Newest first, keyset-paginated: {entries, next}. Pass next as before for the following page; next is null on the last page. Entry: {id, at, action, actor, target, detail}; actor and target are {id, displayName, email}, {id, deleted: true} for a removed account, or null for the system. Login rows (user.login, user.login.failed) are kept 90 days. 400 on an invalid parameter.",
        "parameters": [
          {
            "description": "One action, or a group ending in .* (user.login.* matches user.login and user.login.failed)",
            "in": "query",
            "name": "action",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "User id, as actor or target",
            "in": "query",
            "name": "user",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "RFC 3339 time, inclusive",
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "RFC 3339 time, inclusive",
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Only entries with a smaller id (next of the previous page)",
            "in": "query",
            "name": "before",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Page size, default 100, capped at 500",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Global audit log (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/proposals": {
      "get": {
        "description": "Newest first, at most 500: the proposal fields plus proposer and reviewer ({id, displayName, email}, or null once the account is deleted). 400 on an invalid filter.",
        "parameters": [
          {
            "description": "pending | approved | rejected | revoked",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "hashtag_channel",
            "in": "query",
            "name": "kind",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "List proposals (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/proposals/{id}/approve": {
      "post": {
        "description": "Body {note} (at most 500 characters). Pending only (409 otherwise). The ingestor decrypts the channel from its next refresh (at most a minute) and /api/channels lists it in approvedChannels. 409 when userManagement.channelProposals.maxApproved channels are approved.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Approve a proposal (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/proposals/{id}/reject": {
      "post": {
        "description": "Body {note}. Pending only (409 otherwise). The name cannot be proposed again until the row is pruned, 90 days after the decision.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Reject a proposal (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/proposals/{id}/revoke": {
      "post": {
        "description": "Body {note}. Approved only (409 otherwise). The ingestor stops decrypting the channel from its next refresh unless the channel is configured; stored messages stay. The name can be proposed again.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Revoke an approved proposal (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/prune-geo-filter": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/admin/prune-geo-filter"
      }
    },
    "/api/admin/prune-geo-filter/status": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/admin/prune-geo-filter/status"
      }
    },
    "/api/admin/stats": {
      "get": {
        "description": "Accounts by status, active admins, pending accounts older than 24 hours (stuckPending), bouncing addresses, registrations in 7 and 30 days and per UTC day for 30 days (newPerDay, oldest first), users active in 7 and 30 days, logins and failed logins in 24 hours, mail sent in 7 days by latest delivery status (mail7d), and accounts with 5 or more failed logins in 24 hours (guessing: [{userId, displayName, failed}]).",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "User figures for the admin overview (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users": {
      "get": {
        "parameters": [
          {
            "description": "pending | active | disabled",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "user | admin",
            "in": "query",
            "name": "role",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Substring of email or display name",
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "1 = only addresses whose mail bounces",
            "in": "query",
            "name": "bouncing",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "List users (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users-backup": {
      "get": {
        "description": "A fresh, consistent copy of users.db (VACUUM INTO) as application/octet-stream, attachment corescope-users-\u003cYYYYMMDD-HHMMSS\u003e.db (UTC). It holds every password hash and address: store it encrypted. Records a user.backup audit row. The analyzer database has its own route, GET /api/backup.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Download a users.db snapshot (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}": {
      "delete": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Delete a user (admin)",
        "tags": [
          "users"
        ]
      },
      "get": {
        "description": "{user, sessions, companions, mail, audit}. companions is [{pubkey, name, linkedAt, lastSeenAt}] like GET /api/account/companions.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "User detail with sessions, companions, mail log and audit (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/activate": {
      "post": {
        "description": "For when mail keeps failing. The address stays unverified; recorded as activatedBy + audit row.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Activate a pending user manually (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/disable": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Disable a user (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/enable": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Enable a user (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/mail/{mailId}/refresh": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "mailId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Pull delivery events for one mail from the provider (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/resend-activation": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Resend the activation mail (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/admin/users/{id}/role": {
      "post": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Change a user's role (admin)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/analytics/channels": {
      "get": {
        "description": "Message counts and activity per channel.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Channel analytics",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/distance": {
      "get": {
        "description": "Geographic distance calculations between nodes.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Distance analytics",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/hash-collisions": {
      "get": {
        "description": "Identifies nodes sharing hash prefixes.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Hash collision detection",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/hash-sizes": {
      "get": {
        "description": "Distribution of hash prefix sizes across the network.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Hash size analysis",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/neighbor-graph": {
      "get": {
        "description": "Full neighbor affinity graph for visualization.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Neighbor graph",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/relay-airtime-share": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/analytics/relay-airtime-share"
      }
    },
    "/api/analytics/retransmissions": {
      "get": {
        "description": "Collision-pressure proxy (#1699): per time bucket, the average number of distinct repeaters in the union of all observed paths of each flood event (route types 0/1, TRACE excluded). A transmission's observations are split into flood events at gaps of more than 5 minutes; each event is bucketed by its first observation, and events before the store retention floor are left out. Hop prefixes are not resolved: a prefix counts once per event, so colliding 1-byte prefixes make this a lower bound. Only repeaters some observer heard are counted, so the value also follows observer coverage; each bucket carries its observer count.",
        "parameters": [
          {
            "description": "Comma-separated IATA codes; only observations from observers in the region are counted. A region with no known observers is not filtered",
            "in": "query",
            "name": "region",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Relative window: 1h, 24h, 3d, 7d or 30d",
            "in": "query",
            "name": "window",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Absolute window start (RFC3339)",
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Absolute window end (RFC3339)",
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Bucket size: 5m, 15m, 1h, 6h or 1d (default 1h)",
            "in": "query",
            "name": "bucket",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Retransmission pressure over time",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/rf": {
      "get": {
        "description": "SNR/RSSI distributions and statistics.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "RF analytics",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/roles": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/analytics/roles"
      }
    },
    "/api/analytics/subpath-detail": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Subpath detail",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/subpaths": {
      "get": {
        "description": "Common routing subpaths through the mesh.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Subpath analysis",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/subpaths-bulk": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Bulk subpath analysis",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/analytics/topology": {
      "get": {
        "description": "Hop-count distribution and route analysis.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Network topology",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/audio-lab/buckets": {
      "get": {
        "description": "Returns frequency bucket data for audio analysis.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Audio lab frequency buckets",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/auth/activate": {
      "post": {
        "description": "Request body {token, password}: the mailed activation token and the account password chosen at (the newest) registration. A wrong password answers 401 and leaves the token usable; attempts are rate-limited per account. On success consumes the token, activates the account and starts a session.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Activate an account",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/device-token": {
      "post": {
        "description": "Body {email, password, deviceName}. 200 {token, expiresAt, user: {id, displayName}}. The token is a bearer credential (Authorization: Bearer \u003ctoken\u003e), valid 90 days and extended on use, limited to /api/auth/me, /api/auth/logout, /api/account/settings and /api/account/companions*; other account routes answer 403 to it. deviceName is cleaned (control characters removed, trimmed, at most 64 characters) and shown under Devices. Same 401 answer, rate limits (shared with /api/auth/login) and user.login audit row (detail via=device) as /api/auth/login. No Origin check: nothing is stored in the browser.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Issue a device token (CoreDrive RX)",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/forgot": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Request a password reset",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/login": {
      "post": {
        "description": "Email + password. Sets the cs_session cookie. Rate-limited per IP and per address.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Log in",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/logout": {
      "post": {
        "description": "With the cs_session cookie (Origin must be publicBaseUrl): ends that browser session and clears the cookie. With Authorization: Bearer \u003cdevice token\u003e: revokes that device token.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Log out",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/me": {
      "get": {
        "description": "Returns the logged-in user and the CSRF token, or 401. Also accepts a device token (Authorization: Bearer); csrfToken is then empty.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Current user",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/register": {
      "post": {
        "description": "Creates a pending account and mails an activation link. The response is identical whether or not the address is already registered.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Register an account",
        "tags": [
          "users"
        ]
      }
    },
    "/api/auth/reset": {
      "post": {
        "description": "Consumes the mailed reset token and ends all sessions of the user.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Reset the password",
        "tags": [
          "users"
        ]
      }
    },
    "/api/backup": {
      "get": {
        "description": "Streams a consistent SQLite snapshot of the analyzer DB (VACUUM INTO). Response is application/octet-stream with attachment filename corescope-backup-\u003cunix\u003e.db.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "CookieAuth": []
          }
        ],
        "summary": "Download SQLite backup",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/channels": {
      "get": {
        "description": "Returns known mesh channels with message counts. With userManagement.channelProposals.enabled the response also carries approvedChannels: the approved hashtag channel names, oldest approval first, at most maxApproved, listed also before they have traffic.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "List channels",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/channels/{hash}/messages": {
      "get": {
        "description": "Returns messages for a specific channel.",
        "parameters": [
          {
            "in": "path",
            "name": "hash",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get channel messages",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/config/areas": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/config/areas"
      }
    },
    "/api/config/areas/polygons": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/config/areas/polygons"
      }
    },
    "/api/config/cache": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get cache configuration",
        "tags": [
          "config"
        ]
      }
    },
    "/api/config/client": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get client configuration",
        "tags": [
          "config"
        ]
      }
    },
    "/api/config/geo-filter": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get geo-filter configuration",
        "tags": [
          "config"
        ]
      },
      "put": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/config/geo-filter"
      }
    },
    "/api/config/map": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get map configuration",
        "tags": [
          "config"
        ]
      }
    },
    "/api/config/region-quick-picks": {
      "get": {
        "description": "Named groups of region (IATA) codes from config regionQuickPicks, offered as one-tap choices by the region filter. Always a list; empty when none are configured.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get region quick picks",
        "tags": [
          "config"
        ]
      }
    },
    "/api/config/regions": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get configured regions",
        "tags": [
          "config"
        ]
      }
    },
    "/api/config/theme": {
      "get": {
        "description": "Returns color maps, CSS variables, and theme defaults.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get theme configuration",
        "tags": [
          "config"
        ]
      }
    },
    "/api/debug/affinity": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "CookieAuth": []
          }
        ],
        "summary": "Debug neighbor affinity scores",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/decode": {
      "post": {
        "description": "Decodes a hex-encoded packet without storing it.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Decode a raw packet",
        "tags": [
          "packets"
        ]
      }
    },
    "/api/dropped-packets": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/dropped-packets"
      }
    },
    "/api/health": {
      "get": {
        "description": "Returns server health, uptime, and memory stats.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Health check",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/healthz": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/healthz"
      }
    },
    "/api/iata-coords": {
      "get": {
        "description": "Returns lat/lon for known airport codes (used for observer positioning).",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get IATA airport coordinates",
        "tags": [
          "config"
        ]
      }
    },
    "/api/known-channels": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/known-channels"
      }
    },
    "/api/mqtt/status": {
      "get": {
        "description": "Returns per-MQTT-source connection state and counters (lastConnectUnix, lastPacketUnix, packetsTotal, etc.). Broker URL passwords are masked. Sourced from the ingestor stats file; empty list when unavailable. (#1043)",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "MQTT source status",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/nodes": {
      "get": {
        "description": "Returns all known mesh nodes with status and metadata. Repeater/room rows carry the issue #672 usefulness metrics (traffic_share_score, bridge_score, coverage_score, redundancy_score), the composite usefulness_score + usefulness_grade, and relay-activity counters. See the Node schema.",
        "parameters": [
          {
            "description": "Filter by node role",
            "in": "query",
            "name": "role",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by status (active/stale/offline)",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeListResponse"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "List nodes",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/bulk-health": {
      "get": {
        "description": "Returns health status for all nodes in one call.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Bulk node health",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/clock-skew": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/clock-skew"
      }
    },
    "/api/nodes/network-status": {
      "get": {
        "description": "Returns counts of active, stale, and offline nodes.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Network status summary",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/resolve": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/resolve"
      }
    },
    "/api/nodes/search": {
      "get": {
        "description": "Search nodes by name or public key prefix.",
        "parameters": [
          {
            "description": "Search query",
            "in": "query",
            "name": "q",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Search nodes",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}": {
      "get": {
        "description": "Returns full detail for a single node by public key. For repeater/room nodes this includes the issue #672 usefulness axes + composite score/grade (see the Node schema).",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node detail",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/analytics": {
      "get": {
        "description": "Per-node packet counts, timing, and RF stats.",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node analytics",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/battery": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/{pubkey}/battery"
      }
    },
    "/api/nodes/{pubkey}/clock-skew": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/{pubkey}/clock-skew"
      }
    },
    "/api/nodes/{pubkey}/health": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node health",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/hop_analytics": {
      "get": {
        "description": "One entry per flood packet the node forwarded in the window, with the hop count its flood.max check saw (the node's zero-based index in the path) and tags (flood, scoped or unscoped, advert). DIRECT packets are excluded. Every observation in the window is read. A colliding path prefix is attributed only when the previous hop's neighbor_edges neighbors leave this node as the one candidate; the server's resolved-path pick is not used. Packets carrying this node's prefix that cannot be attributed are counted in `ambiguous`.",
        "parameters": [
          {
            "description": "Lookback window in days (default 7, clamped 1-365)",
            "in": "query",
            "name": "days",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node hop counts",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/neighbors": {
      "get": {
        "description": "Returns the queried node's first-hop neighbors with affinity scores and observation metadata (count, SNR, distance, observers). Ambiguous edges carry candidate pubkeys.",
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeNeighborsResponse"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node neighbors",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/paths": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node routing paths",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/nodes/{pubkey}/reach": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/{pubkey}/reach"
      }
    },
    "/api/nodes/{pubkey}/rx-coverage": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/nodes/{pubkey}/rx-coverage"
      }
    },
    "/api/nodes/{pubkey}/scopes": {
      "get": {
        "description": "Returns the region scopes this repeater has been observed forwarding (matched/unmatched/unscoped, plus route-type mix) alongside its most recently declared region list, if any. declared is null when the repeater has never successfully answered a declared-regions request.",
        "parameters": [
          {
            "description": "Time window: 1h, 24h, or 7d (default 24h)",
            "in": "query",
            "name": "window",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pubkey",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get node scope conformance and declared regions",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/notifications/unsubscribe": {
      "get": {
        "description": "303 to \u003cpublicBaseUrl\u003e/#/account/unsubscribe?token=, the confirm view. Changes nothing, so link scanners cannot unsubscribe anyone.",
        "parameters": [
          {
            "description": "The unsubscribe token from the mail",
            "in": "query",
            "name": "token",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Unsubscribe link (redirect)",
        "tags": [
          "users"
        ]
      },
      "post": {
        "description": "No session and no Origin check: the confirm button and the mail provider's List-Unsubscribe-Post one-click both land here. 200 {ok, message}, also when already off; 410 for an unknown token. The token can do nothing else.",
        "parameters": [
          {
            "description": "The unsubscribe token from the mail",
            "in": "query",
            "name": "token",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Turn notification mails off",
        "tags": [
          "users"
        ]
      }
    },
    "/api/observers": {
      "get": {
        "description": "Returns all known packet observers/gateways.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "List observers",
        "tags": [
          "observers"
        ]
      }
    },
    "/api/observers/clock-skew": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/observers/clock-skew"
      }
    },
    "/api/observers/metrics/summary": {
      "get": {
        "description": "Aggregate metrics across all observers.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Observer metrics summary",
        "tags": [
          "observers"
        ]
      }
    },
    "/api/observers/{id}": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get observer detail",
        "tags": [
          "observers"
        ]
      }
    },
    "/api/observers/{id}/analytics": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get observer analytics",
        "tags": [
          "observers"
        ]
      }
    },
    "/api/observers/{id}/metrics": {
      "get": {
        "description": "Packet rates, uptime, and performance metrics.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get observer metrics",
        "tags": [
          "observers"
        ]
      }
    },
    "/api/packets": {
      "get": {
        "description": "Returns decoded packets with filtering, sorting, and pagination.",
        "parameters": [
          {
            "description": "Max packets to return",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Pagination offset",
            "in": "query",
            "name": "offset",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Sort field",
            "in": "query",
            "name": "sort",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Sort order (asc/desc)",
            "in": "query",
            "name": "order",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by packet type",
            "in": "query",
            "name": "type",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by observer ID",
            "in": "query",
            "name": "observer",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Time range filter (e.g. 1h, 24h, 7d)",
            "in": "query",
            "name": "timeRange",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Full-text search",
            "in": "query",
            "name": "search",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Group duplicate packets by hash",
            "in": "query",
            "name": "groupByHash",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "List packets",
        "tags": [
          "packets"
        ]
      }
    },
    "/api/packets/observations": {
      "post": {
        "description": "Submit multiple observer sightings for existing packets.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Batch submit observations",
        "tags": [
          "packets"
        ]
      }
    },
    "/api/packets/timestamps": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get packet timestamp ranges",
        "tags": [
          "packets"
        ]
      }
    },
    "/api/packets/{id}": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get packet detail",
        "tags": [
          "packets"
        ]
      }
    },
    "/api/paths/inspect": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/paths/inspect"
      }
    },
    "/api/perf": {
      "get": {
        "description": "Returns per-endpoint request timing and slow query log.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Performance statistics",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/perf/io": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/perf/io"
      }
    },
    "/api/perf/reset": {
      "post": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "CookieAuth": []
          }
        ],
        "summary": "Reset performance stats",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/perf/sqlite": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/perf/sqlite"
      }
    },
    "/api/perf/write-sources": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/perf/write-sources"
      }
    },
    "/api/proposals": {
      "post": {
        "description": "Body {kind: \"hashtag_channel\", subject}. subject is trimmed and gets a leading # when missing; at most 31 UTF-8 bytes including the #, no control or invisible characters (ZWJ allowed), not Public. 201 with the proposal {id, kind, subject, status, note, createdAt, decidedAt}. 400 invalid kind or name, 409 already pending, already approved or rejected, or already decrypted on this instance (a name in config.json hashChannels or channelKeys, case-sensitive), 429 when the caller created userManagement.channelProposals.perUserPerDay proposals in 24 hours or maxPending proposals wait for review. Registered only when userManagement.channelProposals.enabled.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "security": [
          {
            "CookieAuth": []
          }
        ],
        "summary": "Propose a hashtag channel",
        "tags": [
          "users"
        ]
      }
    },
    "/api/resolve-hops": {
      "get": {
        "description": "Resolves hash prefixes in a hop path to node names. Returns affinity scores and best candidates.",
        "parameters": [
          {
            "description": "Comma-separated hop hash prefixes",
            "in": "query",
            "name": "hops",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Resolve hop path",
        "tags": [
          "nodes"
        ]
      }
    },
    "/api/rf-noise": {
      "get": {
        "description": "GeoJSON hex cells of the LoRa noise floor measured by mobile clients along their tracks, from client_rf_samples. Lower (more negative) dBm is quieter, the opposite direction to the SNR-coloured coverage layer. Stationary samples are excluded: a parked companion logs hundreds of samples at one point and would otherwise define the cell. Gated on clientRfSamples.",
        "parameters": [
          {
            "description": "Bounding box as minLat,minLon,maxLat,maxLon",
            "in": "query",
            "name": "bbox",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Leaflet zoom level; sets the hex resolution",
            "in": "query",
            "name": "z",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Look-back window in days (1-30, default 7)",
            "in": "query",
            "name": "days",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "RF noise-floor hex grid",
        "tags": [
          "coverage"
        ]
      }
    },
    "/api/rx-coverage": {
      "get": {
        "description": "GeoJSON hex cells where mobile clients directly heard nodes (client_receptions): count, best SNR and the nodes heard per cell. With gaps=1 the payload also carries a `gaps` member: cells a companion drove through (moving client_rf_samples) with no reception in the same window, each with its sample count, plus `gaps_truncated` when capped. Gaps are only returned without a node filter and with clientRfSamples on; they mean nothing was received in this period, not that there is no coverage. Gated on clientRxCoverage.",
        "parameters": [
          {
            "description": "Bounding box as minLat,minLon,maxLat,maxLon",
            "in": "query",
            "name": "bbox",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Leaflet zoom level; sets the hex resolution",
            "in": "query",
            "name": "z",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Look-back window in days (1-30, default 7)",
            "in": "query",
            "name": "days",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Only receptions of this node (pubkey or 2-3 byte prefix); disables gaps",
            "in": "query",
            "name": "node",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Only this companion's receptions (and, with gaps=1, its own track)",
            "in": "query",
            "name": "rx",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "1 = add cells driven through with nothing received",
            "in": "query",
            "name": "gaps",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Mobile RX coverage hex grid",
        "tags": [
          "coverage"
        ]
      }
    },
    "/api/rx-leaderboard": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/rx-leaderboard"
      }
    },
    "/api/scope-audit": {
      "get": {
        "description": "For every repeater that has answered a declared-regions request: the regions it declares, which of those it has NOT been observed forwarding in the window, which scopes it forwards without declaring, and whether it forwards unscoped floods while omitting the '*' wildcard. '*' is never listed as a region — it governs unscoped floods, not a scope. Repeaters never successfully asked are absent rather than shown as declaring nothing. Rows with missing regions sort first; a short window is weak evidence, since a quiet region simply has no traffic.",
        "parameters": [
          {
            "description": "Time window: 1h, 24h, or 7d (default 24h)",
            "in": "query",
            "name": "window",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "declared (default) or transport",
            "in": "query",
            "name": "mode",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "mode=transport only: region scope to split the fleet by",
            "in": "query",
            "name": "scope",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Observer IATA codes, comma-separated: count only forwarding heard by observers there. All means no filter",
            "in": "query",
            "name": "region",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Network-wide scope audit",
        "tags": [
          "analytics"
        ]
      }
    },
    "/api/scope-stats": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "/api/scope-stats"
      }
    },
    "/api/stats": {
      "get": {
        "description": "Returns aggregate stats (node counts, packet counts, observer counts). Cached for 10s.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Network statistics",
        "tags": [
          "admin"
        ]
      }
    },
    "/api/traces/{hash}": {
      "get": {
        "description": "Returns all observer sightings for a packet hash.",
        "parameters": [
          {
            "in": "path",
            "name": "hash",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Success"
          }
        },
        "summary": "Get packet traces",
        "tags": [
          "packets"
        ]
      }
    }
  },
  "tags": [
    {
      "description": "Server administration and diagnostics",
      "name": "admin"
    },
    {
      "description": "Network analytics and statistics",
      "name": "analytics"
    },
    {
      "description": "Mesh channel operations",
      "name": "channels"
    },
    {
      "description": "Server configuration",
      "name": "config"
    },
    {
      "description": "Mesh node operations",
      "name": "nodes"
    },
    {
      "description": "Packet observer/gateway operations",
      "name": "observers"
    },
    {
      "description": "Packet capture and decoding",
      "name": "packets"
    },
    {
      "description": "Optional user management (accounts, sessions, admin)",
      "name": "users"
    }
  ]
}
