mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-02 09:23:44 +00:00
293 lines
15 KiB
Bash
293 lines
15 KiB
Bash
# ─────────────────────────────────────────
|
|
# MeshCore Analytics — Environment Config
|
|
# ─────────────────────────────────────────
|
|
# Copy this file to .env and fill in your values.
|
|
# Never commit .env to source control.
|
|
|
|
# Optional host interface for database, MQTT WebSocket, and Redis port
|
|
# publishing. The loopback default keeps infrastructure private; set this to a
|
|
# trusted LAN interface only when remote access is required and firewalled.
|
|
INFRA_BIND_ADDRESS=127.0.0.1
|
|
# Required for the backend/operator dashboard. Send as
|
|
# `Authorization: Bearer <token>` or `X-Operator-Token`.
|
|
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
OPERATOR_SITE_TOKEN=change_me_64_hex_chars
|
|
|
|
# ── Database ──────────────────────────────────────────────────────────────────
|
|
POSTGRES_DB=meshcore
|
|
POSTGRES_USER=meshcore
|
|
POSTGRES_PASSWORD=change_me_strong_password
|
|
# Must exceed Timescale's configured background-worker count (currently 16)
|
|
# plus launcher/scheduler and logical-replication headroom. Requires a database
|
|
# restart when changed.
|
|
POSTGRES_MAX_WORKER_PROCESSES=24
|
|
|
|
# ── MQTT broker ───────────────────────────────────────────────────────────────
|
|
# Internal docker network URL for Mosquitto WebSocket listener
|
|
MQTT_BROKER_URL=ws://mosquitto:9001
|
|
MQTT_CLIENT_ID=meshcore-analytics-ingest
|
|
|
|
# Credentials for the backend MQTT client
|
|
MQTT_USERNAME=backend
|
|
MQTT_PASSWORD=change_me_strong_password
|
|
|
|
# Bound in-process MQTT work during a burst. The backend uses a persistent
|
|
# session and QoS 1 subscriptions so the broker can replay acknowledged QoS 1
|
|
# publications after a reconnect; the packet batch writer also retries transient
|
|
# database failures.
|
|
MQTT_MAX_PAYLOAD_BYTES=65536
|
|
MQTT_INGEST_CONCURRENCY=8
|
|
MQTT_INGEST_QUEUE_MAX=1000
|
|
|
|
# Archive and remove repeater visibility after its own observer MQTT feed has
|
|
# been silent for one month. Credentials and packet history are preserved.
|
|
STALE_MQTT_OBSERVER_CLEANUP_DAYS=30
|
|
STALE_MQTT_OBSERVER_CLEANUP_INTERVAL_MS=21600000
|
|
|
|
# Bound expensive statistics scans so they cannot exhaust the OLTP pool.
|
|
DATABASE_POOL_MAX=16
|
|
STATS_DB_QUERY_CONCURRENCY=2
|
|
# Aggregate reads remain off until the resumable backfill and shadow comparison
|
|
# have passed. Never leave shadow mode enabled after rollout validation.
|
|
STATS_AGGREGATE_READS_ENABLED=false
|
|
STATS_AGGREGATE_SHADOW_ENABLED=false
|
|
WARMUP_NETWORKS=ukmesh
|
|
PATH_LEARNING_INTERVAL_MS=3600000
|
|
|
|
# Logical storage budgets used by operator health. The backend measures only
|
|
# these mounted data volumes; container root filesystem usage is not reported.
|
|
HEALTH_DATABASE_BUDGET_BYTES=322122547200
|
|
HEALTH_REDIS_BUDGET_BYTES=4294967296
|
|
HEALTH_TERRAIN_BUDGET_BYTES=53687091200
|
|
HEALTH_LOG_BUDGET_BYTES=21474836480
|
|
|
|
# Destructive lifecycle actions are opt-in and additionally require the
|
|
# approval/backup/restore gates documented in docs/db-lifecycle.md.
|
|
DATA_LIFECYCLE_COMPRESSION_ENABLED=false
|
|
DATA_LIFECYCLE_RETENTION_ENABLED=false
|
|
DATA_LIFECYCLE_RETENTION_TARGETS=
|
|
BACKUP_RECEIPT_HOST_DIR=./backup-receipts
|
|
DATA_LIFECYCLE_RECEIPT_MAX_AGE_DAYS=7
|
|
|
|
# ── Redis ─────────────────────────────────────────────────────────────────────
|
|
# Docker Compose protects the internal Redis service with this password. The
|
|
# application passes it separately, so REDIS_URL can remain a plain endpoint.
|
|
REDIS_PASSWORD=change_me_strong_redis_password
|
|
# Optional endpoint override (for an external Redis service). Leave this as the
|
|
# internal service URL when using the bundled Compose stack.
|
|
REDIS_URL=redis://redis:6379
|
|
|
|
# ── JWT ───────────────────────────────────────────────────────────────────────
|
|
# Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
JWT_SECRET=change_me_64_hex_chars
|
|
|
|
# ── CORS / allowed origins ────────────────────────────────────────────────────
|
|
# Comma-separated list of browser origins allowed to access the API and WebSocket.
|
|
# For local development, the defaults in docker-compose.yml cover localhost.
|
|
# Set this to your production domains for deployment.
|
|
ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
|
|
# Per-client public API requests/minute. Keep 120 in production; only isolated
|
|
# load-test projects should raise this.
|
|
API_RATE_LIMIT_MAX=120
|
|
|
|
# ── Frontend hostname (build-time) ────────────────────────────────────────────
|
|
# If you're running two separate frontends (an analytics app and a public website)
|
|
# on different hostnames, set this to the analytics app's hostname.
|
|
# When a visitor hits this hostname they see the analytics dashboard;
|
|
# all other hostnames serve the public website layout.
|
|
# Leave blank to always show the analytics dashboard (simpler single-domain setup).
|
|
VITE_APP_HOSTNAME=app.example.com
|
|
|
|
# Canonical whole-region RF coverage is generated by the pinned HopReach
|
|
# source in third_party/hopreach and exposed only on app.ukmesh.com.
|
|
RF_COVERAGE_ENABLED=true
|
|
HOPREACH_IMAGE=meshcore-analytics-hopreach:local
|
|
HOPREACH_CPUS=4.0
|
|
HOPREACH_CPU_WORKERS=4
|
|
HOPREACH_MEMORY_LIMIT=8g
|
|
|
|
# ── MeshCore channel secrets ──────────────────────────────────────────────────
|
|
# Comma-separated list of AES-128 channel secrets for decrypting GroupText packets.
|
|
# Format: name:hex or bare hex (name defaults to first 6 chars of hex).
|
|
# The default MeshCore public channel key is always included automatically.
|
|
# Example: MESHCORE_CHANNEL_SECRETS=mychannel:9cd4e1f2a3b4c5d6e7f8a9b0c1d2e3f4,aabbccddeeff00112233445566778899
|
|
MESHCORE_CHANNEL_SECRETS=
|
|
|
|
# ── Health check app ──────────────────────────────────────────────────────────
|
|
# Optional MeshCore health-check web app for healthcheck.ukmesh.com.
|
|
# The app subscribes to observer packet MQTT and waits for users to send a
|
|
# generated code to the configured MeshCore channel.
|
|
HEALTHCHECK_APP_TITLE=UKMesh Health Check
|
|
# Reviewed immutable upstream commit. Branches and tags are rejected at build.
|
|
HEALTHCHECK_SOURCE_REF=952eec8e0c55d0749f9abef211c266f992c0bfc7
|
|
HEALTHCHECK_TEST_CHANNEL_NAME=ukmeshtest
|
|
# Optional source entry name in MESHCORE_CHANNEL_SECRETS when the stored secret
|
|
# is named differently from the user-facing channel name.
|
|
HEALTHCHECK_TEST_CHANNEL_SECRET_SOURCE_NAME=test
|
|
# Optional override. If left blank, the container will reuse the matching
|
|
# name:secret entry from MESHCORE_CHANNEL_SECRETS.
|
|
HEALTHCHECK_TEST_CHANNEL_SECRET=
|
|
# Optional: restrict default scoring to a comma-separated list of observer public keys.
|
|
HEALTHCHECK_KNOWN_OBSERVERS=
|
|
# Stock defaults are 900 active / 14400 retained. Set both to 3600 if listed
|
|
# observers should have sent packets, status, or internal MQTT within one hour.
|
|
HEALTHCHECK_OBSERVER_ACTIVE_WINDOW_SECONDS=900
|
|
HEALTHCHECK_OBSERVER_RETENTION_SECONDS=14400
|
|
# Enable these after adding Cloudflare Turnstile keys.
|
|
HEALTHCHECK_TURNSTILE_ENABLED=false
|
|
HEALTHCHECK_TURNSTILE_SITE_KEY=
|
|
HEALTHCHECK_TURNSTILE_SECRET_KEY=
|
|
|
|
# ── Owner portal auth ─────────────────────────────────────────────────────────
|
|
# Map MQTT usernames to owned node public keys (lowercase 64-hex), comma-separated.
|
|
# Format:
|
|
# OWNER_MQTT_USERNAME_MAP=username1=nodeid1|nodeid2,username2=nodeid3
|
|
# Example:
|
|
# OWNER_MQTT_USERNAME_MAP=node1=10cfab...713c95,node-ukmesh=20ed75...fe2ac
|
|
OWNER_MQTT_USERNAME_MAP=hermes-test=A2FF345C45EDDCCB453C9F6B1E9973296932CD4C32EC106541D972BFBF4A4FC5
|
|
# Start in shadow/shadow, review `owner-auth:inventory`, then switch both to
|
|
# enforce/apply only after the inventory has no ambiguous users or missing grants.
|
|
OWNER_AUTHORIZATION_MODE=shadow
|
|
OWNER_ACL_MODE=shadow
|
|
OWNER_ACL_UNMANAGED_USERS=backend,test,test2
|
|
# Explicit exception list for intentionally revoked/disabled accounts that
|
|
# should retain an empty broker stanza during apply cutover.
|
|
OWNER_ACL_ALLOW_EMPTY_USERS=
|
|
|
|
# Bounded analytics controls. These defaults are intentionally conservative;
|
|
# raise them only after reviewing query plans, RSS, and queue latency.
|
|
PATH_RESOLVE_WORKERS=2
|
|
PATH_RESOLVE_BACKGROUND_QUEUE_MAX=128
|
|
PATH_RESOLVE_INTERACTIVE_QUEUE_MAX=32
|
|
PATH_RESOLVE_END_TO_END_TIMEOUT_MS=15000
|
|
PATH_RESOLVE_UNIQUE_INFLIGHT_MAX=34
|
|
PATH_RESOLVE_OBSERVER_VARIANTS_PER_PACKET_MAX=4
|
|
PATH_SINGLE_HISTORY_HOURS=168
|
|
PATH_SINGLE_MAX_OBSERVERS=128
|
|
PATH_MULTI_HISTORY_WINDOW_HOURS=168
|
|
PATH_MULTI_MAX_SCAN_ROWS=2048
|
|
PATH_MULTI_MAX_OBSERVERS=128
|
|
LAZY_PATH_HISTORY_WINDOW_HOURS=168
|
|
LAZY_PATH_MAX_OBSERVATIONS=2048
|
|
LAZY_PATH_MAX_OBSERVERS=128
|
|
LAZY_PATH_MAX_UNIQUE_HASHES=64
|
|
LAZY_PATH_MAX_CANDIDATE_NODES=2048
|
|
PATH_LEARNING_MAX_TRAINING_PACKETS=120000
|
|
PATH_LEARNING_MAX_NODES=50000
|
|
PATH_LEARNING_MAX_LINKS=200000
|
|
PATH_LEARNING_MAX_AGGREGATE_KEYS=1000000
|
|
PATH_LEARNING_RUN_DEADLINE_MS=2700000
|
|
SPAM_MESSAGE_MAX_CANDIDATE_ROWS=10000
|
|
SPAM_MESSAGE_MAX_MESSAGES=2000
|
|
SPAM_MESSAGE_MAX_OBSERVERS=32
|
|
SPAM_MESSAGE_MAX_NORMALIZED_CHARS=512
|
|
SPAM_MESSAGE_MAX_INCIDENTS=100
|
|
SPAM_MESSAGE_MAX_CANDIDATE_CLUSTERS=64
|
|
SPAM_MESSAGE_MAX_EVIDENCE_MEMBERS=256
|
|
SPAM_MESSAGE_ANALYSIS_BUDGET_MS=5000
|
|
SPAM_MESSAGE_DB_TIMEOUT_MS=15000
|
|
STATS_UNIQUE_INFLIGHT_MAX=32
|
|
LINK_QUEUE_V3_PRODUCER_ENABLED=1
|
|
LINK_QUEUE_V3_MAX_JOBS=5000
|
|
LINK_QUEUE_V3_MAX_BYTES=67108864
|
|
LINK_QUEUE_V3_MAX_PAYLOAD_BYTES=32768
|
|
LINK_QUEUE_V3_DEAD_MAX_JOBS=1000
|
|
LINK_QUEUE_V3_DEAD_MAX_BYTES=33554432
|
|
LINK_QUEUE_V3_DEAD_RETENTION_MS=2592000000
|
|
LINK_QUEUE_V3_MAX_ATTEMPTS=5
|
|
LINK_QUEUE_V3_LEASE_MS=120000
|
|
LINK_REBUILD_ADMISSION_TIMEOUT_MS=1800000
|
|
LINK_REBUILD_COMPLETION_TIMEOUT_MS=21600000
|
|
LINK_REBUILD_MAX_NODES=5000
|
|
LINK_REBUILD_MAX_PHYSICAL_JOBS=200000
|
|
LINK_REBUILD_LEASE_MS=120000
|
|
MQTT_AUDIT_MAX_OBSERVATIONS_PER_SCAN=4096
|
|
MQTT_AUDIT_MAX_NODES_PER_USER=256
|
|
MQTT_AUDIT_RETENTION_DAYS=30
|
|
|
|
# ── Public map runtime features ───────────────────────────────────────────────
|
|
# Public map layers are independently controlled at runtime and fail closed.
|
|
# Change these values and recreate only the backend container (no image rebuild);
|
|
# connected clients refresh /api/runtime-config within the configured TTL.
|
|
PUBLIC_FEATURE_INFERRED_NODES_ENABLED=true
|
|
PUBLIC_FEATURE_PACKET_ARCS_ENABLED=true
|
|
PUBLIC_FEATURE_HEATMAP_ENABLED=false
|
|
PUBLIC_FEATURE_CONFIG_TTL_SECONDS=30
|
|
SRTM_MAX_LINK_TILES=64
|
|
SRTM_CACHE_MAX_BYTES=21474836480
|
|
|
|
# Synthetic monitoring and optional alert delivery
|
|
SYNTHETIC_INTERVAL_MS=60000
|
|
SYNTHETIC_FULL_INTERVAL_MS=720000
|
|
SYNTHETIC_SUCCESS_TTL_MS=960000
|
|
SYNTHETIC_TIMEOUT_MS=10000
|
|
SYNTHETIC_INITIAL_STATE_TIMEOUT_MS=35000
|
|
SYNTHETIC_FAILURE_THRESHOLD=3
|
|
ALERT_WEBHOOK_URL=
|
|
MAINTENANCE_ACTIVE=0
|
|
MAINTENANCE_MESSAGE=
|
|
|
|
# Per-client WebSocket safeguards for browser input and slow consumers.
|
|
WS_MAX_PAYLOAD_BYTES=65536
|
|
WS_INITIAL_STATE_DB_TIMEOUT_MS=30000
|
|
WS_MAX_QUEUE_BYTES=8388608
|
|
WS_MAX_BUFFERED_BYTES=33554432
|
|
WS_INITIAL_STATE_MAX_BYTES=16777216
|
|
|
|
# ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
|
|
# Get from: Cloudflare Zero Trust → Access → Tunnels → your tunnel → token
|
|
CLOUDFLARE_TUNNEL_TOKEN=paste_your_tunnel_token_here
|
|
|
|
# ── Anubis bot protection ─────────────────────────────────────────────────────
|
|
# Signing key shared across all Anubis sidecar containers.
|
|
# Generate with: openssl rand -hex 32
|
|
# Keep this secret — it signs the PoW challenge cookies.
|
|
ANUBIS_ED25519_PRIVATE_KEY_HEX=
|
|
|
|
# ── App port ──────────────────────────────────────────────────────────────────
|
|
PORT=3000
|
|
|
|
# ── Message-spam detection (Spam Watch) ───────────────────────────────────────
|
|
# Detects repeated near-duplicate chat messages and groups them into incidents
|
|
# for the public /spam dashboard. All defaults are sensible; see
|
|
# docs/spam-detection.md for the full list and tuning guidance.
|
|
SPAM_MESSAGE_ANALYZER_ENABLED=true
|
|
SPAM_MESSAGE_INTERVAL_MS=300000
|
|
SPAM_MESSAGE_WINDOW_HOURS=24
|
|
# Min distinct near-duplicate messages before a cluster can be an incident.
|
|
# Legit chatter is a few repeats; real spam is dozens+. Raise to reduce false
|
|
# positives from small bursts of normal traffic.
|
|
SPAM_MESSAGE_MIN_TRANSMISSIONS=8
|
|
# Confidence floor for the public list / "ongoing" status (0..1).
|
|
SPAM_MESSAGE_PUBLIC_MIN_SCORE=0.5
|
|
# At/above this many near-duplicate messages a qualified cluster is treated as a
|
|
# sustained automated flood and scored up (unless it is plain test/connectivity
|
|
# traffic). Catches templated floods even when they embed the word "test".
|
|
SPAM_MESSAGE_FLOOD_MIN_MESSAGES=25
|
|
# Channel labels excluded from detection (comma-separated). The test channel
|
|
# carries repeated test traffic by design. NOTE: a flood that also hits other
|
|
# channels (e.g. Public) is still caught via those copies.
|
|
SPAM_MESSAGE_EXCLUDE_CHANNELS=test
|
|
# Origin estimation. Primary method resolves relay PATHS: the first repeater in
|
|
# the path is the source of truth, resolved next to the closest (lowest-hop)
|
|
# receiver. Falls back to the observer-signal estimate (closest-receiver cohort,
|
|
# min-hop + slack) when paths can't be resolved. Per-hop km = radius per hop.
|
|
SPAM_MESSAGE_ORIGIN_USE_PATHS=true
|
|
SPAM_MESSAGE_ORIGIN_PATH_MAX_PACKETS=40
|
|
SPAM_MESSAGE_ORIGIN_PATH_MIN_VOTES=3
|
|
SPAM_MESSAGE_ORIGIN_PATH_CLUSTER_KM=45
|
|
SPAM_MESSAGE_ORIGIN_PATH_AMBIGUOUS_WEIGHT=0.4
|
|
SPAM_MESSAGE_ORIGIN_NEAR_HOP_MAX=2
|
|
SPAM_MESSAGE_ORIGIN_NEAR_HOP_SLACK=1
|
|
SPAM_MESSAGE_ORIGIN_PER_HOP_KM=12
|
|
# Disabled ML learner safeguards (used only when the commented Compose service
|
|
# is deliberately enabled after a shadow-data run).
|
|
MAX_TRAINING_GOLD_ROWS=100000
|
|
ML_LEASE_SECONDS=90
|
|
ML_HEARTBEAT_SECONDS=20
|
|
ML_RUN_DEADLINE_SECONDS=1200
|
|
ML_MODEL_VERSION=lightgbm-path-v1
|
|
ML_DATA_VERSION=gold-multibyte-v2
|
|
# Numeric host group allowed to read the system journal (usually 999).
|
|
JOURNAL_GID=999
|