{"id":35860067,"url":"https://github.com/ipnet-mesh/meshcore-hub","last_synced_at":"2026-05-11T10:03:14.391Z","repository":{"id":327745638,"uuid":"1108754327","full_name":"ipnet-mesh/meshcore-hub","owner":"ipnet-mesh","description":"MeshCore Hub provides a complete solution for monitoring, collecting, and interacting with MeshCore mesh networks","archived":false,"fork":false,"pushed_at":"2026-05-05T13:30:37.000Z","size":2090,"stargazers_count":46,"open_issues_count":4,"forks_count":9,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-05T15:34:59.783Z","etag":null,"topics":["api","claude-ai","dashboard","database","distributed","ipswich","meshcore","mqtt","statistics","suffolk"],"latest_commit_sha":null,"homepage":"https://ipnt.uk","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ipnet-mesh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"buy_me_a_coffee":"jinglemansweep"}},"created_at":"2025-12-02T21:52:34.000Z","updated_at":"2026-05-05T13:31:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ipnet-mesh/meshcore-hub","commit_stats":null,"previous_names":["ipnet-mesh/meshcore-hub"],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/ipnet-mesh/meshcore-hub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipnet-mesh%2Fmeshcore-hub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipnet-mesh%2Fmeshcore-hub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipnet-mesh%2Fmeshcore-hub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipnet-mesh%2Fmeshcore-hub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ipnet-mesh","download_url":"https://codeload.github.com/ipnet-mesh/meshcore-hub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipnet-mesh%2Fmeshcore-hub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32889972,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-10T13:40:02.631Z","status":"online","status_checked_at":"2026-05-11T02:00:05.975Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","claude-ai","dashboard","database","distributed","ipswich","meshcore","mqtt","statistics","suffolk"],"created_at":"2026-01-08T12:12:50.648Z","updated_at":"2026-05-11T10:03:14.373Z","avatar_url":"https://github.com/ipnet-mesh.png","language":"Python","funding_links":["https://buymeacoffee.com/jinglemansweep","https://www.buymeacoffee.com/jinglemansweep"],"categories":[],"sub_categories":[],"readme":"# MeshCore Hub\n\n[![CI](https://github.com/ipnet-mesh/meshcore-hub/actions/workflows/ci.yml/badge.svg)](https://github.com/ipnet-mesh/meshcore-hub/actions/workflows/ci.yml)\n[![Docker](https://github.com/ipnet-mesh/meshcore-hub/actions/workflows/docker.yml/badge.svg)](https://github.com/ipnet-mesh/meshcore-hub/actions/workflows/docker.yml)\n[![codecov](https://codecov.io/github/ipnet-mesh/meshcore-hub/graph/badge.svg?token=DO4F82DLKS)](https://codecov.io/github/ipnet-mesh/meshcore-hub)\n[![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://www.buymeacoffee.com/jinglemansweep)\n\nPython 3.14+ platform for managing and orchestrating MeshCore mesh networks.\n\n\u003e [!WARNING]\n\u003e **BREAKING CHANGES** - The latest release replaces Mosquitto with a JWT-based MQTT broker, removes the proprietary receiver service in favor of [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture), and renames `receiver_node_id` to `observer_node_id` in the database. If upgrading from a previous version, see [docs/upgrading.md](docs/upgrading.md) for migration steps.\n\n![MeshCore Hub Web Dashboard](docs/images/web.png)\n\n\u003e [!IMPORTANT]\n\u003e **Help Translate MeshCore Hub** 🌍\n\u003e\n\u003e We need volunteers to translate the web dashboard! Currently only English is available. Check out the [Translation Guide](docs/i18n.md) to contribute a language pack. Partial translations welcome!\n\n## Overview\n\nMeshCore Hub provides a complete solution for monitoring, collecting, and interacting with MeshCore mesh networks. Data ingestion is handled by [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture), which observes MeshCore RF traffic and publishes decoded packets to MQTT. It consists of multiple components that work together:\n\n| Component         | Description                                                  |\n| ----------------- | ------------------------------------------------------------ |\n| **Collector**     | Subscribes to MQTT events and persists them to a database    |\n| **API**           | REST API for querying data                                   |\n| **Web Dashboard** | Single Page Application (SPA) for visualizing network status |\n\n## Architecture\n\n```mermaid\nflowchart LR\n    subgraph Devices[\"MeshCore Devices\"]\n        D1[\"Device 1\"]\n        D2[\"Device 2\"]\n        D3[\"Device 3\"]\n    end\n\n    PCAP[\"meshcore-packet-capture\"]\n\n    D1 -.-\u003e|RF| PCAP\n    D2 -.-\u003e|RF| PCAP\n    D3 -.-\u003e|RF| PCAP\n\n    PCAP --\u003e|Publish| MQTT[\"MQTT Broker\"]\n\n    subgraph Backend[\"Backend Services\"]\n        Collector --\u003e Database --\u003e API\n    end\n\n    MQTT --\u003e Collector\n    API --\u003e Web[\"Web Dashboard\"]\n\n    style Devices fill:none,stroke:#0288d1,stroke-width:2px\n    style PCAP fill:none,stroke:#f57c00,stroke-width:2px\n    style Backend fill:none,stroke:#388e3c,stroke-width:2px\n    style MQTT fill:none,stroke:#7b1fa2,stroke-width:3px\n    style Collector fill:none,stroke:#388e3c,stroke-width:2px\n    style Database fill:none,stroke:#c2185b,stroke-width:2px\n    style API fill:none,stroke:#1976d2,stroke-width:2px\n    style Web fill:none,stroke:#ffa000,stroke-width:2px\n```\n\n## Features\n\n- **Event Persistence**: Store messages, advertisements, telemetry, and trace data\n- **REST API**: Query historical data with filtering and pagination\n- **Node Tagging**: Add custom metadata to nodes for organization\n- **Web Dashboard**: Visualize network status, node locations, and message history\n- **Internationalization**: Full i18n support with composable translation patterns\n- **Docker Ready**: Single image with all components, easy deployment\n\n## Getting Started\n\n### Docker Compose Profiles\n\nDocker Compose uses **profiles** to select which services to run. The configuration is split across multiple files:\n\n| File                         | Purpose                                                            |\n| ---------------------------- | ------------------------------------------------------------------ |\n| `docker-compose.yml`         | Base shared config (services, profiles, healthchecks, environment) |\n| `docker-compose.dev.yml`     | Development overrides (port mappings for direct access)            |\n| `docker-compose.prod.yml`    | Production overrides (external proxy network, no exposed ports)    |\n| `docker-compose.traefik.yml` | Optional Traefik auto-discovery labels                             |\n\nAll `docker compose` commands require explicit file selection with `-f`:\n\n```bash\n# Development (default — exposes ports for local access)\ndocker compose -f docker-compose.yml -f docker-compose.dev.yml --profile all up -d\n\n# Production (generic reverse proxy — nginx, caddy, etc.)\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml --profile all up -d\n\n# Production (Traefik)\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.traefik.yml --profile all up -d\n```\n\nService profiles:\n\n| Profile    | Services                        | Use Case                                  |\n| ---------- | ------------------------------- | ----------------------------------------- |\n| `all`      | mqtt, observer, migrate, collector, api, web | Everything on one host        |\n| `core`     | migrate, collector, api, web                 | Central server infrastructure |\n| `mqtt`     | meshcore-mqtt-broker            | Local MQTT broker (optional)              |\n| `observer` | packet capture observer         | Observes RF traffic and publishes to MQTT |\n| `seed`     | seed                            | One-time seed data import                 |\n| `migrate`  | migrate                         | One-time database migration               |\n\n**Note:** Most deployments connect to an external MQTT broker. Add `--profile mqtt` only if you need a local broker. The `observer` profile runs [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) to observe MeshCore RF traffic and publish decoded packets to MQTT.\n\n### Simple Self-Hosted Setup\n\nThe quickest way to get started is running the entire stack on a single machine with a connected LoRa radio.\n\n**Prerequisites:**\n\n1. A compatible LoRa radio (e.g., Heltec V3, T-Beam) connected via serial\n\n**Steps:**\n\n```bash\n# Create a directory, download the Docker Compose files and\n# example environment configuration file\n\nmkdir meshcore-hub\ncd meshcore-hub\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.yml\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.dev.yml\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/.env.example\n\n# Copy and configure environment\ncp .env.example .env\n# Edit .env: set PACKETCAPTURE_IATA to your 3-letter airport code\n#            set SERIAL_PORT if not /dev/ttyUSB0\n\n# Start the entire stack with local MQTT broker and packet capture\ndocker compose -f docker-compose.yml -f docker-compose.dev.yml --profile mqtt --profile core --profile observer up -d\n\n# View the web dashboard\nopen http://localhost:8080\n```\n\nThis starts all services: MQTT broker, collector, API, web dashboard, and packet capture. The `observer` profile runs [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) to observe MeshCore RF traffic and publish decoded packets to MQTT.\n\n\u003e **ARM / Raspberry Pi:** The `observer` service Docker image (`ghcr.io/agessaman/meshcore-packet-capture`) does not support 32-bit ARM (`armv7l`) architectures. Modern Raspberry Pi models (3, 4, 5) using a 64-bit OS are fully supported. If you are running a 32-bit ARM system, install [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) natively and configure it to publish to your MQTT broker instead of using the `--profile observer` Docker profile.\n\n## Deployment\n\n### Production Setup\n\nFor production deployments, use `docker-compose.prod.yml` which connects services to an external proxy network. No ports are exposed directly — all traffic goes through your reverse proxy.\n\n**Prerequisites:**\n\n1. A reverse proxy (Nginx Proxy Manager, Caddy, Traefik, etc.)\n2. Docker network for proxy communication\n\n**Steps:**\n\n```bash\n# Create proxy network (once)\ndocker network create proxy-net\n\n# Download compose files and config\nmkdir meshcore-hub \u0026\u0026 cd meshcore-hub\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.yml\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.prod.yml\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/.env.example\ncp .env.example .env\n# Edit .env: set COMPOSE_PROJECT_NAME, MQTT credentials, API keys, etc.\n\n# Start core services\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml --profile core up -d\n\n# Or include local MQTT broker\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core up -d\n\n# Or include packet capture on the same host\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core --profile observer up -d\n```\n\nConfigure your reverse proxy to forward to the containers:\n\n| Service        | Container                     | Port | Path                             |\n| -------------- | ----------------------------- | ---- | -------------------------------- |\n| Web Dashboard  | `{COMPOSE_PROJECT_NAME}-web`  | 8080 | `/`                              |\n| API            | `{COMPOSE_PROJECT_NAME}-api`  | 8000 | `/api`, `/metrics`, `/health`    |\n| MQTT WebSocket | `{COMPOSE_PROJECT_NAME}-mqtt` | 1883 | `/` (only if using local broker) |\n\n\u003e **Important:** Do not host under a subpath (e.g., `/meshcore`). Proxy at `/`.\n\n#### Reverse Proxy\n\nMeshCore Hub is designed to run behind a reverse proxy in production. A Traefik override file is provided with pre-configured labels:\n\n```bash\n# Download the Traefik override\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.traefik.yml\n\n# Set your domain in .env\necho \"TRAEFIK_DOMAIN=meshcore.example.com\" \u003e\u003e .env\n\n# Start with Traefik labels\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.traefik.yml --profile core up -d\n```\n\nThis routes the web dashboard and API to `TRAEFIK_DOMAIN` with automatic TLS.\n\n#### Multi-Instance Deployments\n\nTo run multiple Hub instances (e.g., production + staging) on the same Docker host, set `TRAEFIK_PRIORITY` to control which router wins when domains overlap. Higher values are matched first:\n\n```bash\n# Production (.env)\nCOMPOSE_PROJECT_NAME=hub\nTRAEFIK_DOMAIN=example.com\nTRAEFIK_PRIORITY=10\n\n# Staging (.env) — separate directory with its own config\nCOMPOSE_PROJECT_NAME=hub-beta\nTRAEFIK_DOMAIN=beta.example.com\nTRAEFIK_PRIORITY=20\n```\n\nThis ensures `beta.example.com` (priority 20) is matched before the production wildcard `*.example.com` (priority 10). For other services on the same network (e.g., an MQTT broker at `mqtt.example.com`), use an even higher priority (e.g., 30).\n\n### Adding Remote Observers\n\nOther operators can run their own [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) instance and publish decoded packets to your MeshCore Hub. They can also optionally contribute to the LetsMesh network.\n\n\u003e **Prerequisite:** Your MQTT broker must be accessible to remote observers. In production, this means exposing the WebSocket listener via a reverse proxy with TLS (e.g., `wss://mqtt.example.com/mqtt`).\n\n#### Example: Observer contributing to LetsMesh and your community Hub\n\nA ready-made Docker Compose setup is provided in `contrib/packetcapture/`. Download it and configure:\n\n```bash\nmkdir meshcore-observer \u0026\u0026 cd meshcore-observer\n\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/main/contrib/packetcapture/docker-compose.yml\nwget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/main/contrib/packetcapture/.env.example\n\ncp .env.example .env\n```\n\nEdit `.env` and update the following variables:\n\n| Variable | Description |\n|----------|-------------|\n| `SERIAL_PORT` | Device path for your Meshtastic device (e.g. `/dev/ttyUSB0`, or `/dev/serial/by-id/...` for a stable path) |\n| `IATA` | 3-letter area code for your location (e.g. `STN`, `SEA`) |\n| `ORIGIN` | Observer identifier (default: `observer`) |\n| `ENABLE_LETSMESH_US` | Set `true` to contribute to LetsMesh US |\n| `ENABLE_LETSMESH_EU` | Set `true` to contribute to LetsMesh EU |\n| `CUSTOM_MQTT_HOST` | Your community Hub's public MQTT domain (e.g. `mqtt.example.com`) |\n| `CUSTOM_MQTT_PORT` | MQTT port — `443` for TLS/WSS, `1883` for local/plain |\n| `CUSTOM_MQTT_TLS` | `true` for TLS, `false` for plain connections |\n| `CUSTOM_MQTT_AUDIENCE` | Must match `MQTT_TOKEN_AUDIENCE` on your broker (e.g. `mqtt.example.com` for TLS, `mqtt.localhost` for local) |\n\nThen start the observer:\n\n```bash\ndocker compose up -d\n```\n\n\u003e **Local network (no TLS):** Set `CUSTOM_MQTT_HOST` to the Hub's LAN IP (e.g. `192.168.1.100`), `CUSTOM_MQTT_PORT=1883`, `CUSTOM_MQTT_TLS=false`, and `CUSTOM_MQTT_AUDIENCE=mqtt.localhost`.\n\n### Backup \u0026 Restore\n\n#### Using Makefile\n\n```bash\n# Back up all volumes to backup/\nmake backup\n\n# Restore a specific volume\nmake restore FILE=backup/hub_data-20260414-120000.tar.gz\n```\n\n#### Using shell commands\n\n```bash\n# Back up the database volume\nsource .env 2\u003e/dev/null || true\nmkdir -p backup\nvol=${COMPOSE_PROJECT_NAME:-hub}_data\ndocker run --rm -v $vol:/data -v $(pwd)/backup:/backup \\\n  alpine tar czf /backup/$vol-$(date +%Y%m%d-%H%M%S).tar.gz -C / data\n\n# Restore a specific volume (volume name derived from tarball filename)\nsource .env 2\u003e/dev/null || true\nFILE=backup/${COMPOSE_PROJECT_NAME:-hub}_data-20260414-120000.tar.gz\nvol=$(basename \"$FILE\" | sed 's/-[0-9]\\{8\\}-[0-9]\\{6\\}\\.tar\\.gz//')\ndocker run --rm -v $vol:/data -v $(pwd)/backup:/backup \\\n  alpine sh -c \"cd / \u0026\u0026 tar xzf /backup/$(basename $FILE)\"\n```\n\n\u003e **Note:** Replace `hub` with your `COMPOSE_PROJECT_NAME` if using a different instance name. Monitoring infrastructure (Prometheus, Alertmanager) manages its own data — consult your monitoring stack's documentation for backup procedures.\n\n## Configuration\n\nAll components are configured via environment variables. Create a `.env` file or export variables:\n\n### Common Settings\n\n| Variable         | Default      | Description                                                 |\n| ---------------- | ------------ | ----------------------------------------------------------- |\n| `LOG_LEVEL`      | `INFO`       | Logging level (DEBUG, INFO, WARNING, ERROR)                 |\n| `DATA_HOME`      | `./data`     | Base directory for runtime data                             |\n| `SEED_HOME`      | `./seed`     | Directory containing seed data files                        |\n| `MQTT_HOST`      | `localhost`  | MQTT broker hostname                                        |\n| `MQTT_PORT`      | `1883`       | MQTT broker port                                            |\n| `MQTT_USERNAME`  | _(none)_     | MQTT username (optional)                                    |\n| `MQTT_PASSWORD`  | _(none)_     | MQTT password (optional)                                    |\n| `MQTT_PREFIX`    | `meshcore`   | Topic prefix for all MQTT messages                          |\n| `MQTT_TLS`       | `false`      | Enable TLS/SSL for MQTT connection                          |\n\n\u003e **Note:** `MQTT_PREFIX` also accepts the legacy alias `MQTT_TOPIC_PREFIX` for backward compatibility.\n\n### Collector Settings\n\n| Variable                         | Default  | Description                                                          |\n| -------------------------------- | -------- | -------------------------------------------------------------------- |\n| `COLLECTOR_CHANNEL_KEYS`         | _(none)_ | Additional decoder channel keys (`label=hex`, `label:hex`, or `hex`) |\n| `COLLECTOR_INCLUDE_TEST_CHANNEL` | `false`  | Include built-in 'test' channel messages                             |\n\n#### LetsMesh Packet Decoding\n\nFor details on how the collector normalizes and decodes LetsMesh packets, see [docs/letsmesh.md](docs/letsmesh.md).\n\n### OIDC Authentication\n\nThe web dashboard supports OIDC/OAuth2 authentication. When enabled (`OIDC_ENABLED=true`), the admin interface requires users to authenticate with an identity provider (e.g. LogTo, Keycloak) and have the `admin` role assigned. See [docs/auth.md](docs/auth.md) for setup instructions, configuration reference, and IdP-specific guides.\n\n### Webhooks\n\nThe collector can forward events (advertisements, messages) to external HTTP endpoints via webhooks with configurable URLs, secrets, retries, and timeouts. See [docs/webhooks.md](docs/webhooks.md) for the full configuration reference and payload format.\n\n### Data Retention\n\nThe collector automatically cleans up old event data and inactive nodes:\n\n| Variable                        | Default | Description                              |\n| ------------------------------- | ------- | ---------------------------------------- |\n| `DATA_RETENTION_ENABLED`        | `true`  | Enable automatic cleanup of old events   |\n| `DATA_RETENTION_DAYS`           | `30`    | Days to retain event data                |\n| `DATA_RETENTION_INTERVAL_HOURS` | `24`    | Hours between cleanup runs               |\n| `NODE_CLEANUP_ENABLED`          | `true`  | Enable removal of inactive nodes         |\n| `NODE_CLEANUP_DAYS`             | `30`    | Remove nodes not seen for this many days |\n\n### API Settings\n\n| Variable            | Default   | Description                                             |\n| ------------------- | --------- | ------------------------------------------------------- |\n| `API_HOST`          | `0.0.0.0` | API bind address                                        |\n| `API_PORT`          | `8000`    | API port                                                |\n| `API_READ_KEY`      | _(none)_  | Read-only API key                                       |\n| `API_ADMIN_KEY`     | _(none)_  | Admin API key                                           |\n| `METRICS_ENABLED`   | `true`    | Enable Prometheus metrics endpoint at `/metrics`        |\n| `METRICS_CACHE_TTL` | `60`      | Seconds to cache metrics output (reduces database load) |\n| `CORS_ORIGINS`      | _(none)_  | Comma-separated list of allowed CORS origins for the API (optional, only needed when the web dashboard runs on a different origin) |\n\n### Web Dashboard Settings\n\n| Variable                   | Default                 | Description                                                                                                                                                                                                                                  |\n| -------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `WEB_HOST`                 | `0.0.0.0`               | Web server bind address                                                                                                                                                                                                                      |\n| `WEB_PORT`                 | `8080`                  | Web server port                                                                                                                                                                                                                              |\n| `API_BASE_URL`             | `http://localhost:8000` | API endpoint URL                                                                                                                                                                                                                             |\n| `API_KEY`                  | _(none)_                | API key for web dashboard queries (optional)                                                                                                                                                                                                 |\n| `WEB_THEME`                | `dark`                  | Default theme (`dark` or `light`). Users can override via theme toggle in navbar.                                                                                                                                                            |\n| `WEB_LOCALE`               | `en`                    | Locale/language for the web dashboard (e.g., `en`, `es`, `fr`)                                                                                                                                                                               |\n| `WEB_DATETIME_LOCALE`      | `en-US`                 | Locale used for date formatting in the web dashboard (e.g., `en-US` for MM/DD/YYYY, `en-GB` for DD/MM/YYYY).                                                                                                                                 |\n| `WEB_AUTO_REFRESH_SECONDS` | `30`                    | Auto-refresh interval in seconds for list pages (0 to disable)                                                                                                                                                                               |\n| `WEB_DEBUG`                | `false`                 | Enable debug mode in the web dashboard (shows extra diagnostic info)                                                                                                                                                                          |\n| `OIDC_ENABLED`             | `false`                 | Enable OIDC authentication for the web dashboard                                                                                                                                                                                              |\n| `OIDC_CLIENT_ID`           | _(none)_                | OIDC client ID (from IdP, required when OIDC_ENABLED=true)                                                                                                                                                                                    |\n| `OIDC_CLIENT_SECRET`       | _(none)_                | OIDC client secret (from IdP, required when OIDC_ENABLED=true)                                                                                                                                                                                |\n| `OIDC_DISCOVERY_URL`       | _(none)_                | IdP base URL — `.well-known/openid-configuration` is appended automatically (required when OIDC_ENABLED=true)                                                                                                                                |\n| `OIDC_REDIRECT_URI`        | _(auto-derived)_        | Explicit callback URL (overrides auto-derivation from request)                                                                                                                                                                                |\n| `OIDC_POST_LOGOUT_REDIRECT_URI` | _(auto-derived)_  | Post-logout redirect URI (must match Sign-out redirect URIs in IdP). Falls back to `OIDC_REDIRECT_URI` base or `request.base_url`                                                                                                           |\n| `OIDC_SCOPES`              | `openid email profile`  | OAuth scopes to request. The `openid` scope is required for ID tokens. Quotes are stripped automatically.  |\n| `OIDC_ROLES_CLAIM`         | `roles`                 | ID token claim name containing user roles                                                                                                                                                                                                     |\n| `OIDC_ROLE_ADMIN`          | `admin`                 | IdP role name granting admin access                                                                                                                                                                                                           |\n| `OIDC_ROLE_OPERATOR`       | `operator`              | IdP role name for operator access (future use)                                                                                                                                                                                                |\n| `OIDC_ROLE_MEMBER`         | `member`                | IdP role name for member access                                                                                                                                                                                                               |\n| `OIDC_SESSION_SECRET`      | _(none)_                | Secret for signing session cookies (required when OIDC_ENABLED=true)                                                                                                                                                                          |\n| `OIDC_SESSION_MAX_AGE`     | `86400`                 | Session cookie lifetime in seconds (default 24 hours)                                                                                                                                                                                         |\n| `OIDC_COOKIE_SECURE`       | `false`                 | HTTPS-only session cookies (enable in production)                                                                                                                                                                                             |\n| `TZ`                       | `UTC`                   | Timezone for displaying dates/times (e.g., `America/New_York`, `Europe/London`)                                                                                                                                                              |\n| `NETWORK_DOMAIN`           | _(none)_                | Network domain name (optional)                                                                                                                                                                                                               |\n| `NETWORK_NAME`             | `MeshCore Network`      | Display name for the network                                                                                                                                                                                                                 |\n| `NETWORK_CITY`             | _(none)_                | City where network is located                                                                                                                                                                                                                |\n| `NETWORK_COUNTRY`          | _(none)_                | Country code (ISO 3166-1 alpha-2)                                                                                                                                                                                                            |\n| `NETWORK_RADIO_CONFIG`     | _(none)_                | Radio config (comma-delimited: profile,freq,bw,sf,cr,power)                                                                                                                                                                                  |\n| `NETWORK_WELCOME_TEXT`     | _(none)_                | Custom welcome text for homepage                                                                                                                                                                                                             |\n| `NETWORK_CONTACT_EMAIL`    | _(none)_                | Contact email address                                                                                                                                                                                                                        |\n| `NETWORK_CONTACT_DISCORD`  | _(none)_                | Discord server link                                                                                                                                                                                                                          |\n| `NETWORK_CONTACT_GITHUB`   | _(none)_                | GitHub repository URL                                                                                                                                                                                                                        |\n| `NETWORK_CONTACT_YOUTUBE`  | _(none)_                | YouTube channel URL                                                                                                                                                                                                                          |\n| `CONTENT_HOME`             | `./content`             | Directory containing custom content (pages/, media/)                                                                                                                                                                                         |\n\nTimezone handling note:\n\n- API timestamps that omit an explicit timezone suffix are treated as UTC before rendering in the configured `TZ`.\n\n### Feature Flags\n\nControl which pages are visible in the web dashboard. Disabled features are fully hidden: removed from navigation, return 404 on their routes, and excluded from sitemap/robots.txt.\n\n| Variable                 | Default | Description                                           |\n| ------------------------ | ------- | ----------------------------------------------------- |\n| `FEATURE_DASHBOARD`      | `true`  | Enable the `/dashboard` page                          |\n| `FEATURE_NODES`          | `true`  | Enable the `/nodes` pages (list, detail, short links) |\n| `FEATURE_ADVERTISEMENTS` | `true`  | Enable the `/advertisements` page                     |\n| `FEATURE_MESSAGES`       | `true`  | Enable the `/messages` page                           |\n| `FEATURE_MAP`            | `true`  | Enable the `/map` page and `/map/data` endpoint       |\n| `FEATURE_MEMBERS`        | `true`  | Enable the `/members` page                            |\n| `FEATURE_PAGES`          | `true`  | Enable custom markdown pages                          |\n\n**Dependencies:** Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. Members auto-disables when OIDC is disabled (set via `OIDC_ENABLED`).\n\n### Custom Content\n\nThe web dashboard supports custom markdown pages and media files (including custom logos) served from a configurable content directory. See [docs/content.md](docs/content.md) for the full setup guide including directory structure, frontmatter fields, and Docker volume mounting.\n\n## Seed Data\n\nThe database can be seeded with node tags from YAML files. See [docs/seeding.md](docs/seeding.md) for format details, directory structure, and running the seed process.\n\n## API Documentation\n\nWhen running, the API provides interactive documentation at:\n\n- **Swagger UI**: http://localhost:8000/api/docs\n- **ReDoc**: http://localhost:8000/api/redoc\n- **OpenAPI JSON**: http://localhost:8000/api/openapi.json\n\nHealth check endpoints are also available:\n\n- **Health**: http://localhost:8000/health\n- **Ready**: http://localhost:8000/health/ready (includes database check)\n- **Metrics**: http://localhost:8000/metrics (Prometheus format — point your Prometheus scraper here)\n\n### Authentication\n\nThe API supports optional bearer token authentication:\n\n```bash\n# Read-only access\ncurl -H \"Authorization: Bearer \u003cAPI_READ_KEY\u003e\" http://localhost:8000/api/v1/nodes\n\n# Admin access\ncurl -H \"Authorization: Bearer \u003cAPI_ADMIN_KEY\u003e\" http://localhost:8000/api/v1/members\n```\n\nThe web dashboard supports OIDC/OAuth2 authentication for admin access. When enabled, users must authenticate with an identity provider and have the `admin` role assigned. See [docs/auth.md](docs/auth.md) for setup instructions and IdP-specific guides.\n\n### Example Endpoints\n\n| Method | Endpoint                             | Description                       |\n| ------ | ------------------------------------ | --------------------------------- |\n| GET    | `/api/v1/nodes`                      | List all known nodes              |\n| GET    | `/api/v1/nodes/{public_key}`         | Get node details                  |\n| GET    | `/api/v1/nodes/prefix/{prefix}`      | Get node by public key prefix     |\n| GET    | `/api/v1/nodes/{public_key}/tags`    | Get node tags                     |\n| POST   | `/api/v1/nodes/{public_key}/tags`    | Create node tag                   |\n| GET    | `/api/v1/messages`                   | List messages with filters        |\n| GET    | `/api/v1/advertisements`             | List advertisements               |\n| GET    | `/api/v1/telemetry`                  | List telemetry data               |\n| GET    | `/api/v1/trace-paths`                | List trace paths                  |\n| GET    | `/api/v1/members`                    | List network members              |\n| GET    | `/api/v1/dashboard/stats`            | Get network statistics            |\n| GET    | `/api/v1/dashboard/activity`         | Get daily advertisement activity  |\n| GET    | `/api/v1/dashboard/message-activity` | Get daily message activity        |\n| GET    | `/api/v1/dashboard/node-count`       | Get cumulative node count history |\n\n## Development\n\n### Setup\n\n```bash\n# Clone and setup\ngit clone https://github.com/ipnet-mesh/meshcore-hub.git\ncd meshcore-hub\n\n# Install frontend dependencies and build static assets (requires Node.js 22+ LTS)\nnpm install\nnpm run build\n\n# Setup Python environment\npython -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev]\"\n\n# Install pre-commit hooks\npre-commit install\n\n# Run database migrations\nmeshcore-hub db upgrade\n\n# Start components (in separate terminals)\nmeshcore-hub collector\nmeshcore-hub api\nmeshcore-hub web\n```\n\n\u003e **Note:** `npm run build` compiles Tailwind CSS and copies vendor libraries (lit-html, Leaflet, Chart.js, QRCode.js) into `src/meshcore_hub/web/static/vendor/`. This step is required before the web dashboard will render correctly. In Docker, this happens automatically during the build.\n\n### Running Tests\n\n```bash\n# Run all tests\npytest\n\n# Run with coverage\npytest --cov=meshcore_hub --cov-report=html\n\n# Run specific test file\npytest tests/test_api/test_nodes.py\n\n# Run tests matching pattern\npytest -k \"test_list\"\n```\n\n### Code Quality\n\n```bash\n# Run all code quality checks (formatting, linting, type checking)\npre-commit run --all-files\n```\n\n### Creating Database Migrations\n\n```bash\n# Auto-generate migration from model changes\nmeshcore-hub db revision --autogenerate -m \"Add new field to nodes\"\n\n# Create empty migration\nmeshcore-hub db revision -m \"Custom migration\"\n\n# Apply migrations\nmeshcore-hub db upgrade\n```\n\n## Project Structure\n\n```\nmeshcore-hub/\n├── src/meshcore_hub/       # Main package\n│   ├── common/             # Shared code (models, schemas, config)\n│   ├── collector/          # MQTT event collector\n│   ├── api/                # REST API\n│   └── web/                # Web dashboard\n│       ├── templates/      # Jinja2 templates (SPA shell)\n│       └── static/\n│           ├── css/         # Stylesheets (app.css, input.css, built tailwind.css)\n│           ├── vendor/      # Vendored JS/CSS libraries (built by npm run build)\n│           ├── js/spa/      # SPA frontend (ES modules, lit-html)\n│           └── locales/     # Translation files (en.json)\n├── tests/                  # Test suite\n├── alembic/                # Database migrations\n├── etc/                    # Configuration files (MQTT, Prometheus, Alertmanager)\n├── example/                # Example files for reference\n│   ├── seed/               # Example seed data files\n│   │   └── node_tags.yaml  # Example node tags\n│   └── content/            # Example custom content\n│       ├── pages/          # Example custom pages\n│       │   └── join.md     # Example join page\n│       └── media/          # Example media files\n│           └── images/     # Custom images\n├── seed/                   # Seed data directory (SEED_HOME, copy from example/seed/)\n├── content/                # Custom content directory (CONTENT_HOME, optional)\n│   ├── pages/              # Custom markdown pages\n│   └── media/              # Custom media files\n│       └── images/         # Custom images (logo.svg/png/jpg/jpeg/webp replace default logo)\n├── data/                   # Runtime data directory (DATA_HOME, created at runtime)\n├── Dockerfile              # Docker build configuration (multi-stage: Node.js frontend + Python)\n├── package.json            # Frontend build dependencies (Tailwind, DaisyUI, lit-html, etc.)\n├── build.js                # Frontend build script (Tailwind CLI + vendor copy)\n├── docker-compose.yml      # Docker Compose base config\n├── docker-compose.dev.yml  # Development overrides (port mappings)\n├── docker-compose.prod.yml # Production overrides (proxy network)\n├── docker-compose.traefik.yml # Optional Traefik labels\n├── docs/                    # Documentation\n│   ├── images/              # Screenshots and images\n│   ├── hosting/             # Reverse proxy hosting guides\n│   ├── content.md           # Custom content setup guide\n│   ├── i18n.md              # Translation reference guide\n│   ├── letsmesh.md          # LetsMesh packet decoding details\n│   ├── seeding.md           # Seed data format and import guide\n│   ├── upgrading.md         # Upgrade guide for breaking changes\n│   └── webhooks.md          # Webhook configuration reference\n├── SCHEMAS.md               # Event schema documentation\n└── AGENTS.md                # AI assistant guidelines\n```\n\n## Documentation\n\n- [SCHEMAS.md](SCHEMAS.md) - MeshCore event schemas\n- [docs/upgrading.md](docs/upgrading.md) - Upgrade guide for breaking changes\n- [docs/letsmesh.md](docs/letsmesh.md) - LetsMesh packet decoding details\n- [docs/seeding.md](docs/seeding.md) - Seed data format and import guide\n- [docs/i18n.md](docs/i18n.md) - Translation reference guide\n- [docs/content.md](docs/content.md) - Custom content setup guide\n- [docs/auth.md](docs/auth.md) - OIDC authentication setup and configuration\n- [docs/webhooks.md](docs/webhooks.md) - Webhook configuration reference\n- [AGENTS.md](AGENTS.md) - Guidelines for AI coding assistants\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Run tests and quality checks (`pytest \u0026\u0026 pre-commit run --all-files`)\n5. Commit your changes (`git commit -m 'Add amazing feature'`)\n6. Push to the branch (`git push origin feature/amazing-feature`)\n7. Open a Pull Request\n\n## License\n\nThis project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See [LICENSE](LICENSE) for details.\n\n## Acknowledgments\n\n- [MeshCore](https://meshcore.io/) - The mesh networking protocol\n- [meshcore](https://github.com/fdlamotte/meshcore) - Python library for MeshCore devices\n- [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) - RF packet capture and MQTT publisher for data ingestion\n- [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker) - WebSocket MQTT broker with MeshCore public key authentication. The Docker image (`ghcr.io/ipnet-mesh/meshcore-mqtt-broker`) is built and published by a GitHub Action in this repository that clones the upstream source, as the upstream project does not currently provide a public Docker image (although a [PR has been submitted](https://github.com/michaelhart/meshcore-mqtt-broker/pull/1) to add this).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fipnet-mesh%2Fmeshcore-hub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fipnet-mesh%2Fmeshcore-hub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fipnet-mesh%2Fmeshcore-hub/lists"}