diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..925dc7a --- /dev/null +++ b/.env.example @@ -0,0 +1,41 @@ +# MeshExplorer unified stack configuration. +# Copy this file to .env and fill in the values, then run: +# docker compose up --build +# (add `--profile bot` to also start the Discord relay). + +# ─── ClickHouse ────────────────────────────────────────────────────────────── +# The read/write "default" user is used by the ingest daemon and the migration +# runner. Set a real password before deploying. +CLICKHOUSE_DB=default +CLICKHOUSE_USER=default +CLICKHOUSE_PASSWORD=changeme + +# Read-only user used by the web app and the Discord bot. This account is only +# reachable on the internal docker network; the default matches ingest/clickhouse/users.xml. +CLICKHOUSE_READONLY_USER=readonly +CLICKHOUSE_READONLY_PASSWORD=readonly + +# ─── MeshCore MQTT ingest ──────────────────────────────────────────────────── +# JSON array of MQTT brokers to subscribe to for meshcore packets. Each entry: +# { "url": "...", "username": "...", "password": "...", "topics": ["meshcore/#"] } +# "topics" is optional and defaults to ["meshcore/#"]. The ingest daemon exits +# with an error if this is empty, so configure at least one broker. +MQTT_BROKERS=[{"url":"tcp://mqtt.example.com:1883","username":"CHANGE_ME","password":"CHANGE_ME","topics":["meshcore/#"]}] +MQTT_CLIENT_ID=meshcore-ingest + +# ─── Web app ───────────────────────────────────────────────────────────────── +# Base URL for client-side API calls. Leave empty to use relative URLs. +NEXT_PUBLIC_API_URL= + +# ─── Discord relay bot (optional, --profile bot) ───────────────────────────── +# Required when running the bot. Create a webhook in your Discord server. +DISCORD_WEBHOOK_URL= +# Optional: post into a specific thread instead of the channel. +DISCORD_THREAD_ID= +# Region filter for messages (e.g. seattle). +MESH_REGION=seattle +# Poll interval (ms) and batch size. +POLL_INTERVAL=300 +MAX_ROWS_PER_POLL=50 +# Comma-separated base64 private keys used to decrypt channel messages. +PRIVATE_KEYS= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c10427 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Local environment configuration (never commit real secrets) +.env +.env.* +!.env.example + +# OS files +.DS_Store + +# Go build artifacts from the ingest module +/ingest/meshcoreingest +/ingest/migrate +/ingest/clickhouse-meshingest +*.test +*.out diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e5fdeb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,134 @@ +services: + # ClickHouse database (custom image bundles the meshcore decrypt UDF) + clickhouse: + build: ./ingest/clickhouse + environment: + - CLICKHOUSE_DB=${CLICKHOUSE_DB:-default} + - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-} + - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 + ports: + # Published to localhost only, for debugging. Not required by the stack. + - "127.0.0.1:8123:8123" + - "127.0.0.1:9000:9000" + volumes: + - clickhouse-data:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "clickhouse-client --user $$CLICKHOUSE_USER --password $$CLICKHOUSE_PASSWORD --query 'SELECT 1' || exit 1"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + restart: unless-stopped + networks: + - meshnet + + # One-shot migration runner (goose). Applies the meshcore schema then exits. + migrate: + build: ./ingest + command: + - "./migrate" + - "-host" + - "clickhouse" + - "-port" + - "9000" + - "-database" + - "${CLICKHOUSE_DB:-default}" + - "-username" + - "${CLICKHOUSE_USER:-default}" + - "-password" + - "${CLICKHOUSE_PASSWORD:-}" + - "-path" + - "migrations" + - "-action" + - "up" + depends_on: + clickhouse: + condition: service_healthy + restart: "no" + networks: + - meshnet + + # MeshCore MQTT -> ClickHouse ingest daemon + meshcoreingest: + build: ./ingest + command: ["./meshcoreingest"] + environment: + - CLICKHOUSE_HOST=clickhouse + - CLICKHOUSE_PORT=9000 + - CLICKHOUSE_DB=${CLICKHOUSE_DB:-default} + - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-} + - MQTT_BROKERS=${MQTT_BROKERS} + - MQTT_CLIENT_ID=${MQTT_CLIENT_ID:-meshcore-ingest} + depends_on: + clickhouse: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped + networks: + - meshnet + + # MeshExplorer web app (Next.js). Reads ClickHouse over HTTP (8123) as the + # readonly user. + meshexplorer: + build: + context: ./meshexplorer + dockerfile: Dockerfile + environment: + - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 + - CLICKHOUSE_HOST=clickhouse + - CLICKHOUSE_PORT=8123 + - CLICKHOUSE_USER=${CLICKHOUSE_READONLY_USER:-readonly} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_READONLY_PASSWORD:-readonly} + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-} + ports: + - "3001:3000" + depends_on: + clickhouse: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped + init: true + networks: + - meshnet + + # MeshCore -> Discord relay bot. Optional: enabled with the "bot" profile. + # docker compose --profile bot up + discord-bot: + build: + context: ./meshexplorer + dockerfile: Dockerfile.bot + profiles: ["bot"] + environment: + - NODE_ENV=production + - CLICKHOUSE_HOST=clickhouse + - CLICKHOUSE_PORT=8123 + - CLICKHOUSE_USER=${CLICKHOUSE_READONLY_USER:-readonly} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_READONLY_PASSWORD:-readonly} + - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} + - DISCORD_THREAD_ID=${DISCORD_THREAD_ID:-} + - MESH_REGION=${MESH_REGION:-seattle} + - POLL_INTERVAL=${POLL_INTERVAL:-300} + - MAX_ROWS_PER_POLL=${MAX_ROWS_PER_POLL:-50} + - PRIVATE_KEYS=${PRIVATE_KEYS:-} + depends_on: + clickhouse: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped + init: true + networks: + - meshnet + +volumes: + clickhouse-data: + +networks: + meshnet: + driver: bridge diff --git a/meshexplorer/docker-compose.yml b/meshexplorer/docker-compose.yml deleted file mode 100644 index 8a754bc..0000000 --- a/meshexplorer/docker-compose.yml +++ /dev/null @@ -1,59 +0,0 @@ -version: '3.8' - -services: - meshexplorer: - build: - context: . - dockerfile: Dockerfile - ports: - - "3001:3000" - environment: - # Next.js Configuration - - NODE_ENV=production - - PORT=3000 - - HOSTNAME=0.0.0.0 - - # ClickHouse Database Configuration - - CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-clickhouse} - - CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-8123} - - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default} - - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} - - # Next.js API Configuration - - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-} - restart: unless-stopped - init: true - networks: - - shared-network - - discord-bot: - build: - context: . - dockerfile: Dockerfile.bot - environment: - # Node.js Configuration - - NODE_ENV=production - - # ClickHouse Database Configuration - - CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-clickhouse} - - CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-8123} - - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default} - - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} - - # Discord Bot Configuration - - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} - - DISCORD_THREAD_ID=${DISCORD_THREAD_ID:-} - - MESH_REGION=${MESH_REGION:-seattle} - - POLL_INTERVAL=${POLL_INTERVAL:-1000} - - MAX_ROWS_PER_POLL=${MAX_ROWS_PER_POLL:-50} - - PRIVATE_KEYS=${PRIVATE_KEYS:-} - restart: unless-stopped - init: true - networks: - - shared-network - depends_on: - - meshexplorer - -networks: - shared-network: - external: true \ No newline at end of file