Refine environment configuration defaults (#318)

This commit is contained in:
l5y
2025-10-13 14:06:14 +02:00
committed by GitHub
parent ea9c633eff
commit dee6ad7e4a
25 changed files with 403 additions and 317 deletions
+7 -15
View File
@@ -26,29 +26,28 @@ CONNECTION=/dev/ttyACM0
SITE_NAME=My Meshtastic Network
# Default Meshtastic channel
DEFAULT_CHANNEL=#MediumFast
CHANNEL=#LongFast
# Default frequency for your region
# Common frequencies: 868MHz (Europe), 915MHz (US), 433MHz (Worldwide)
DEFAULT_FREQUENCY=868MHz
FREQUENCY=915MHz
# Map center coordinates (latitude, longitude)
# Berlin, Germany: 52.502889, 13.404194
# Denver, Colorado: 39.7392, -104.9903
# London, UK: 51.5074, -0.1278
MAP_CENTER_LAT=52.502889
MAP_CENTER_LON=13.404194
MAP_CENTER="38.761944,-27.090833"
# Maximum distance to show nodes (kilometers)
MAX_NODE_DISTANCE_KM=50
MAX_DISTANCE=42
# =============================================================================
# OPTIONAL INTEGRATIONS
# =============================================================================
# Matrix chat room for your community (optional)
# Format: !roomid:matrix.org
MATRIX_ROOM='#meshtastic-berlin:matrix.org'
# Community chat link or Matrix room for your community (optional)
# Matrix aliases (e.g. #meshtastic-berlin:matrix.org) will be linked via matrix.to automatically.
CONTACT_LINK='#potatomesh:dod.ngo'
# =============================================================================
@@ -70,10 +69,3 @@ POTATOMESH_IMAGE_ARCH=linux-amd64
# Meshtastic channel index (0=primary, 1=secondary, etc.)
CHANNEL_INDEX=0
# Database settings
DB_BUSY_TIMEOUT_MS=5000
DB_BUSY_MAX_RETRIES=5
DB_BUSY_RETRY_DELAY=0.05
# Application settings
MAX_JSON_BODY_BYTES=1048576
+2 -2
View File
@@ -35,8 +35,8 @@ CONNECTION=/dev/ttyACM0
Additional environment variables are optional:
- `DEFAULT_CHANNEL`, `DEFAULT_FREQUENCY`, `MAP_CENTER_LAT`, `MAP_CENTER_LON`,
`MAX_NODE_DISTANCE_KM`, and `MATRIX_ROOM` customise the UI.
- `CHANNEL`, `FREQUENCY`, `MAP_CENTER`, `MAX_DISTANCE`, and `CONTACT_LINK`
customise the UI.
- `POTATOMESH_INSTANCE` (defaults to `http://web:41447`) lets the ingestor post
to a remote PotatoMesh instance if you do not run both services together.
- `CONNECTION` overrides the default serial device or network endpoint used by
+8 -13
View File
@@ -54,8 +54,8 @@ COPY --chown=potatomesh:potatomesh web/views/ ./views/
COPY --chown=potatomesh:potatomesh data/*.sql /data/
# Create data directory for SQLite database
RUN mkdir -p /app/data && \
chown -R potatomesh:potatomesh /app/data
RUN mkdir -p /app/data /app/.local/share/potato-mesh && \
chown -R potatomesh:potatomesh /app/data /app/.local
# Switch to non-root user
USER potatomesh
@@ -65,18 +65,13 @@ EXPOSE 41447
# Default environment variables (can be overridden by host)
ENV APP_ENV=production \
MESH_DB=/app/data/mesh.db \
DB_BUSY_TIMEOUT_MS=5000 \
DB_BUSY_MAX_RETRIES=5 \
DB_BUSY_RETRY_DELAY=0.05 \
MAX_JSON_BODY_BYTES=1048576 \
RACK_ENV=production \
SITE_NAME="PotatoMesh Demo" \
DEFAULT_CHANNEL="#LongFast" \
DEFAULT_FREQUENCY="915MHz" \
MAP_CENTER_LAT=38.761944 \
MAP_CENTER_LON=-27.090833 \
MAX_NODE_DISTANCE_KM=42 \
MATRIX_ROOM="#potatomesh:dod.ngo" \
CHANNEL="#LongFast" \
FREQUENCY="915MHz" \
MAP_CENTER="38.761944,-27.090833" \
MAX_DISTANCE=42 \
CONTACT_LINK="#potatomesh:dod.ngo" \
DEBUG=0
# Start the application
+7 -8
View File
@@ -84,13 +84,12 @@ well-known document is staged in
The web app can be configured with environment variables (defaults shown):
* `SITE_NAME` - title and header shown in the UI (default: "PotatoMesh Demo")
* `DEFAULT_CHANNEL` - default channel shown in the UI (default: "#LongFast")
* `DEFAULT_FREQUENCY` - default frequency shown in the UI (default: "915MHz")
* `MAP_CENTER_LAT` / `MAP_CENTER_LON` - default map center coordinates (default: `38.761944` / `-27.090833`)
* `MAX_NODE_DISTANCE_KM` - hide nodes farther than this distance from the center (default: `42`)
* `MATRIX_ROOM` - matrix room id for a footer link (default: `#potatomesh:dod.ngo`)
* `CHANNEL` - default channel shown in the UI (default: "#LongFast")
* `FREQUENCY` - default frequency shown in the UI (default: "915MHz")
* `MAP_CENTER` - default map center coordinates (default: `38.761944,-27.090833`)
* `MAX_DISTANCE` - hide nodes farther than this distance from the center (default: `42`)
* `CONTACT_LINK` - chat link or Matrix alias for footer and overlay (default: `#potatomesh:dod.ngo`)
* `PRIVATE` - set to `1` to hide the chat UI, disable message APIs, and exclude hidden clients (default: unset)
* `PROM_REPORT_IDS` - comma-separated list of Node IDs to report in prometheus metrics, `*` for all (default: unset)
The application derives SEO-friendly document titles, descriptions, and social
preview tags from these existing configuration values and reuses the bundled
@@ -99,7 +98,7 @@ logo for Open Graph and Twitter cards.
Example:
```bash
SITE_NAME="PotatoMesh Demo" MAP_CENTER_LAT=38.761944 MAP_CENTER_LON=-27.090833 MAX_NODE_DISTANCE_KM=42 MATRIX_ROOM="#potatomesh:dod.ngo" ./app.sh
SITE_NAME="PotatoMesh Demo" MAP_CENTER=38.761944,-27.090833 MAX_DISTANCE=42 CONTACT_LINK="#potatomesh:dod.ngo" ./app.sh
```
### API
@@ -183,5 +182,5 @@ See the [Docker guide](DOCKER.md) for more details and custome deployment instru
Apache v2.0, Contact <COM0@l5y.tech>
Join our Matrix to discuss the dashboard or ask for technical support:
Join our community chat to discuss the dashboard or ask for technical support:
[#potatomesh:dod.ngo](https://matrix.to/#/#potatomesh:dod.ngo)
+20 -23
View File
@@ -68,32 +68,30 @@ update_env() {
# Get current values from .env if they exist
SITE_NAME=$(grep "^SITE_NAME=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "PotatoMesh Demo")
DEFAULT_CHANNEL=$(grep "^DEFAULT_CHANNEL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "#LongFast")
DEFAULT_FREQUENCY=$(grep "^DEFAULT_FREQUENCY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "915MHz")
MAP_CENTER_LAT=$(grep "^MAP_CENTER_LAT=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "38.761944")
MAP_CENTER_LON=$(grep "^MAP_CENTER_LON=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "-27.090833")
MAX_NODE_DISTANCE_KM=$(grep "^MAX_NODE_DISTANCE_KM=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "42")
MATRIX_ROOM=$(grep "^MATRIX_ROOM=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "#potatomesh:dod.ngo")
CHANNEL=$(grep "^CHANNEL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "#LongFast")
FREQUENCY=$(grep "^FREQUENCY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "915MHz")
MAP_CENTER=$(grep "^MAP_CENTER=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "38.761944,-27.090833")
MAX_DISTANCE=$(grep "^MAX_DISTANCE=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "42")
CONTACT_LINK=$(grep "^CONTACT_LINK=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "#potatomesh:dod.ngo")
API_TOKEN=$(grep "^API_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "")
POTATOMESH_IMAGE_ARCH=$(grep "^POTATOMESH_IMAGE_ARCH=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "linux-amd64")
echo "📍 Location Settings"
echo "-------------------"
read_with_default "Site Name (your mesh network name)" "$SITE_NAME" SITE_NAME
read_with_default "Map Center Latitude" "$MAP_CENTER_LAT" MAP_CENTER_LAT
read_with_default "Map Center Longitude" "$MAP_CENTER_LON" MAP_CENTER_LON
read_with_default "Max Node Distance (km)" "$MAX_NODE_DISTANCE_KM" MAX_NODE_DISTANCE_KM
read_with_default "Map Center (lat,lon)" "$MAP_CENTER" MAP_CENTER
read_with_default "Max Distance (km)" "$MAX_DISTANCE" MAX_DISTANCE
echo ""
echo "📡 Meshtastic Settings"
echo "---------------------"
read_with_default "Default Channel" "$DEFAULT_CHANNEL" DEFAULT_CHANNEL
read_with_default "Default Frequency (868MHz, 915MHz, etc.)" "$DEFAULT_FREQUENCY" DEFAULT_FREQUENCY
read_with_default "Channel" "$CHANNEL" CHANNEL
read_with_default "Frequency (868MHz, 915MHz, etc.)" "$FREQUENCY" FREQUENCY
echo ""
echo "💬 Optional Settings"
echo "-------------------"
read_with_default "Matrix Room (optional, e.g., #meshtastic-berlin:matrix.org)" "$MATRIX_ROOM" MATRIX_ROOM
read_with_default "Chat link or Matrix room (optional)" "$CONTACT_LINK" CONTACT_LINK
echo ""
echo "🛠 Docker Settings"
@@ -137,12 +135,11 @@ echo "📝 Updating .env file..."
# Update .env file
update_env "SITE_NAME" "\"$SITE_NAME\""
update_env "DEFAULT_CHANNEL" "\"$DEFAULT_CHANNEL\""
update_env "DEFAULT_FREQUENCY" "\"$DEFAULT_FREQUENCY\""
update_env "MAP_CENTER_LAT" "$MAP_CENTER_LAT"
update_env "MAP_CENTER_LON" "$MAP_CENTER_LON"
update_env "MAX_NODE_DISTANCE_KM" "$MAX_NODE_DISTANCE_KM"
update_env "MATRIX_ROOM" "\"$MATRIX_ROOM\""
update_env "CHANNEL" "\"$CHANNEL\""
update_env "FREQUENCY" "\"$FREQUENCY\""
update_env "MAP_CENTER" "\"$MAP_CENTER\""
update_env "MAX_DISTANCE" "$MAX_DISTANCE"
update_env "CONTACT_LINK" "\"$CONTACT_LINK\""
update_env "API_TOKEN" "$API_TOKEN"
update_env "POTATOMESH_IMAGE_ARCH" "$POTATOMESH_IMAGE_ARCH"
@@ -172,11 +169,11 @@ echo "✅ Configuration complete!"
echo ""
echo "📋 Your settings:"
echo " Site Name: $SITE_NAME"
echo " Map Center: $MAP_CENTER_LAT, $MAP_CENTER_LON"
echo " Max Distance: ${MAX_NODE_DISTANCE_KM}km"
echo " Channel: $DEFAULT_CHANNEL"
echo " Frequency: $DEFAULT_FREQUENCY"
echo " Matrix Room: ${MATRIX_ROOM:-'Not set'}"
echo " Map Center: $MAP_CENTER"
echo " Max Distance: ${MAX_DISTANCE}km"
echo " Channel: $CHANNEL"
echo " Frequency: $FREQUENCY"
echo " Chat: ${CONTACT_LINK:-'Not set'}"
echo " API Token: ${API_TOKEN:0:8}..."
echo " Docker Image Arch: $POTATOMESH_IMAGE_ARCH"
echo ""
+4 -2
View File
@@ -5,7 +5,7 @@ services:
DEBUG: 1
volumes:
- ./web:/app
- ./data:/data
- ./data:/app/.local/share/potato-mesh
- /app/vendor/bundle
web-bridge:
@@ -13,7 +13,7 @@ services:
DEBUG: 1
volumes:
- ./web:/app
- ./data:/data
- ./data:/app/.local/share/potato-mesh
- /app/vendor/bundle
ports:
- "41447:41447"
@@ -24,6 +24,7 @@ services:
DEBUG: 1
volumes:
- ./data:/app
- ./data:/app/.local/share/potato-mesh
- /app/.local
ingestor-bridge:
@@ -31,4 +32,5 @@ services:
DEBUG: 1
volumes:
- ./data:/app
- ./data:/app/.local/share/potato-mesh
- /app/.local
+7 -8
View File
@@ -4,17 +4,16 @@ x-web-base: &web-base
APP_ENV: ${APP_ENV:-production}
RACK_ENV: ${RACK_ENV:-production}
SITE_NAME: ${SITE_NAME:-PotatoMesh Demo}
DEFAULT_CHANNEL: ${DEFAULT_CHANNEL:-#LongFast}
DEFAULT_FREQUENCY: ${DEFAULT_FREQUENCY:-915MHz}
MAP_CENTER_LAT: ${MAP_CENTER_LAT:-38.761944}
MAP_CENTER_LON: ${MAP_CENTER_LON:--27.090833}
MAX_NODE_DISTANCE_KM: ${MAX_NODE_DISTANCE_KM:-42}
MATRIX_ROOM: ${MATRIX_ROOM:-#potatomesh:dod.ngo}
CHANNEL: ${CHANNEL:-#LongFast}
FREQUENCY: ${FREQUENCY:-915MHz}
MAP_CENTER: ${MAP_CENTER:-38.761944,-27.090833}
MAX_DISTANCE: ${MAX_DISTANCE:-42}
CONTACT_LINK: ${CONTACT_LINK:-#potatomesh:dod.ngo}
API_TOKEN: ${API_TOKEN}
DEBUG: ${DEBUG:-0}
command: ["ruby", "app.rb", "-p", "41447", "-o", "0.0.0.0"]
volumes:
- potatomesh_data:/app/data
- potatomesh_data:/app/.local/share/potato-mesh
- potatomesh_logs:/app/logs
restart: unless-stopped
deploy:
@@ -35,7 +34,7 @@ x-ingestor-base: &ingestor-base
API_TOKEN: ${API_TOKEN}
DEBUG: ${DEBUG:-0}
volumes:
- potatomesh_data:/app/data
- potatomesh_data:/app/.local/share/potato-mesh
- potatomesh_logs:/app/logs
devices:
- ${CONNECTION:-/dev/ttyACM0}:${CONNECTION:-/dev/ttyACM0}
+5 -11
View File
@@ -68,18 +68,12 @@ ENV RACK_ENV=production \
APP_ENV=production \
XDG_DATA_HOME=/app/.local/share \
XDG_CONFIG_HOME=/app/.config \
MESH_DB=/app/data/mesh.db \
DB_BUSY_TIMEOUT_MS=5000 \
DB_BUSY_MAX_RETRIES=5 \
DB_BUSY_RETRY_DELAY=0.05 \
MAX_JSON_BODY_BYTES=1048576 \
SITE_NAME="PotatoMesh Demo" \
DEFAULT_CHANNEL="#LongFast" \
DEFAULT_FREQUENCY="915MHz" \
MAP_CENTER_LAT=38.761944 \
MAP_CENTER_LON=-27.090833 \
MAX_NODE_DISTANCE_KM=42 \
MATRIX_ROOM="#potatomesh:dod.ngo" \
CHANNEL="#LongFast" \
FREQUENCY="915MHz" \
MAP_CENTER="38.761944,-27.090833" \
MAX_DISTANCE=42 \
CONTACT_LINK="#potatomesh:dod.ngo" \
DEBUG=0
# Start the application
+1 -4
View File
@@ -18,7 +18,4 @@ set -euo pipefail
bundle install
PORT=${PORT:-41447}
BIND_ADDRESS=${BIND_ADDRESS:-0.0.0.0}
exec ruby app.rb -p "${PORT}" -o "${BIND_ADDRESS}"
exec ruby app.rb -p 41447 -o 0.0.0.0
+5 -6
View File
@@ -78,6 +78,9 @@ module PotatoMesh
register App::Routes::Ingest
register App::Routes::Root
DEFAULT_PORT = 41_447
DEFAULT_BIND_ADDRESS = "0.0.0.0"
APP_VERSION = determine_app_version
INSTANCE_PRIVATE_KEY, INSTANCE_KEY_GENERATED = load_or_generate_instance_private_key
INSTANCE_PUBLIC_KEY_PEM = INSTANCE_PRIVATE_KEY.public_key.export
@@ -98,12 +101,7 @@ module PotatoMesh
#
# @param default_port [Integer] fallback port when ENV['PORT'] is absent or invalid.
# @return [Integer] port number for the HTTP server.
def self.resolve_port(default_port: 41_447)
raw = ENV["PORT"]
return default_port if raw.nil?
Integer(raw, 10)
rescue ArgumentError
def self.resolve_port(default_port: DEFAULT_PORT)
default_port
end
@@ -112,6 +110,7 @@ module PotatoMesh
set :views, File.expand_path("../../views", __dir__)
set :federation_thread, nil
set :port, resolve_port
set :bind, DEFAULT_BIND_ADDRESS
app_logger = PotatoMesh::Logging.build_logger($stdout)
set :logger, app_logger
@@ -31,8 +31,8 @@ module PotatoMesh
pubkey: app_constant(:INSTANCE_PUBLIC_KEY_PEM),
name: sanitized_site_name,
version: app_constant(:APP_VERSION),
channel: sanitized_default_channel,
frequency: sanitized_default_frequency,
channel: sanitized_channel,
frequency: sanitized_frequency,
latitude: PotatoMesh::Config.map_center_lat,
longitude: PotatoMesh::Config.map_center_lon,
last_update_time: last_update,
+22 -13
View File
@@ -89,18 +89,18 @@ module PotatoMesh
PotatoMesh::Sanitizer.sanitized_site_name
end
# Retrieve the configured default channel.
# Retrieve the configured channel.
#
# @return [String] sanitised channel identifier.
def sanitized_default_channel
PotatoMesh::Sanitizer.sanitized_default_channel
def sanitized_channel
PotatoMesh::Sanitizer.sanitized_channel
end
# Retrieve the configured default frequency descriptor.
# Retrieve the configured frequency descriptor.
#
# @return [String] sanitised frequency text.
def sanitized_default_frequency
PotatoMesh::Sanitizer.sanitized_default_frequency
def sanitized_frequency
PotatoMesh::Sanitizer.sanitized_frequency
end
# Build the configuration hash exposed to the frontend application.
@@ -111,23 +111,32 @@ module PotatoMesh
refreshIntervalSeconds: PotatoMesh::Config.refresh_interval_seconds,
refreshMs: PotatoMesh::Config.refresh_interval_seconds * 1000,
chatEnabled: !private_mode?,
defaultChannel: sanitized_default_channel,
defaultFrequency: sanitized_default_frequency,
channel: sanitized_channel,
frequency: sanitized_frequency,
contactLink: sanitized_contact_link,
contactLinkUrl: sanitized_contact_link_url,
mapCenter: {
lat: PotatoMesh::Config.map_center_lat,
lon: PotatoMesh::Config.map_center_lon,
},
maxNodeDistanceKm: PotatoMesh::Config.max_node_distance_km,
maxDistanceKm: PotatoMesh::Config.max_distance_km,
tileFilters: PotatoMesh::Config.tile_filters,
instanceDomain: app_constant(:INSTANCE_DOMAIN),
}
end
# Retrieve the configured Matrix room or nil when unset.
# Retrieve the configured contact link or nil when unset.
#
# @return [String, nil] Matrix room identifier.
def sanitized_matrix_room
PotatoMesh::Sanitizer.sanitized_matrix_room
# @return [String, nil] contact link identifier.
def sanitized_contact_link
PotatoMesh::Sanitizer.sanitized_contact_link
end
# Retrieve the hyperlink derived from the configured contact link.
#
# @return [String, nil] hyperlink pointing to the community chat.
def sanitized_contact_link_url
PotatoMesh::Sanitizer.sanitized_contact_link_url
end
# Retrieve the configured maximum node distance in kilometres.
@@ -26,15 +26,16 @@ module PotatoMesh
lastNodeUpdate: last_update,
config: {
siteName: sanitized_site_name,
defaultChannel: sanitized_default_channel,
defaultFrequency: sanitized_default_frequency,
channel: sanitized_channel,
frequency: sanitized_frequency,
contactLink: sanitized_contact_link,
contactLinkUrl: sanitized_contact_link_url,
refreshIntervalSeconds: PotatoMesh::Config.refresh_interval_seconds,
mapCenter: {
lat: PotatoMesh::Config.map_center_lat,
lon: PotatoMesh::Config.map_center_lon,
},
maxNodeDistanceKm: PotatoMesh::Config.max_node_distance_km,
matrixRoom: sanitized_matrix_room,
maxDistanceKm: PotatoMesh::Config.max_distance_km,
instanceDomain: app_constant(:INSTANCE_DOMAIN),
privateMode: private_mode?,
},
@@ -53,12 +53,13 @@ module PotatoMesh
meta_title: meta[:title],
meta_name: meta[:name],
meta_description: meta[:description],
default_channel: sanitized_default_channel,
default_frequency: sanitized_default_frequency,
channel: sanitized_channel,
frequency: sanitized_frequency,
map_center_lat: PotatoMesh::Config.map_center_lat,
map_center_lon: PotatoMesh::Config.map_center_lon,
max_node_distance_km: PotatoMesh::Config.max_node_distance_km,
matrix_room: sanitized_matrix_room,
max_distance_km: PotatoMesh::Config.max_distance_km,
contact_link: sanitized_contact_link,
contact_link_url: sanitized_contact_link_url,
version: app_constant(:APP_VERSION),
private_mode: private_mode?,
refresh_interval_seconds: PotatoMesh::Config.refresh_interval_seconds,
+77 -35
View File
@@ -18,6 +18,21 @@ module PotatoMesh
module Config
module_function
DEFAULT_DB_BUSY_TIMEOUT_MS = 5_000
DEFAULT_DB_BUSY_MAX_RETRIES = 5
DEFAULT_DB_BUSY_RETRY_DELAY = 0.05
DEFAULT_MAX_JSON_BODY_BYTES = 1_048_576
DEFAULT_REFRESH_INTERVAL_SECONDS = 60
DEFAULT_TILE_FILTER_LIGHT = "grayscale(1) saturate(0) brightness(0.92) contrast(1.05)"
DEFAULT_TILE_FILTER_DARK = "grayscale(1) invert(1) brightness(0.9) contrast(1.08)"
DEFAULT_MAP_CENTER_LAT = 38.761944
DEFAULT_MAP_CENTER_LON = -27.090833
DEFAULT_MAP_CENTER = "#{DEFAULT_MAP_CENTER_LAT},#{DEFAULT_MAP_CENTER_LON}"
DEFAULT_CHANNEL = "#LongFast"
DEFAULT_FREQUENCY = "915MHz"
DEFAULT_CONTACT_LINK = "#potatomesh:dod.ngo"
DEFAULT_MAX_DISTANCE_KM = 42.0
# Resolve the absolute path to the web application root directory.
#
# @return [String] absolute filesystem path of the web folder.
@@ -65,28 +80,28 @@ module PotatoMesh
#
# @return [String] absolute path to the database file.
def db_path
ENV.fetch("MESH_DB", default_db_path)
default_db_path
end
# Retrieve the SQLite busy timeout duration in milliseconds.
#
# @return [Integer] timeout value in milliseconds.
def db_busy_timeout_ms
ENV.fetch("DB_BUSY_TIMEOUT_MS", "5000").to_i
DEFAULT_DB_BUSY_TIMEOUT_MS
end
# Retrieve the maximum number of retries when encountering SQLITE_BUSY.
#
# @return [Integer] maximum retry attempts.
def db_busy_max_retries
ENV.fetch("DB_BUSY_MAX_RETRIES", "5").to_i
DEFAULT_DB_BUSY_MAX_RETRIES
end
# Retrieve the backoff delay between busy retries in seconds.
#
# @return [Float] seconds to wait between retries.
def db_busy_retry_delay
ENV.fetch("DB_BUSY_RETRY_DELAY", "0.05").to_f
DEFAULT_DB_BUSY_RETRY_DELAY
end
# Convenience constant describing the number of seconds in a week.
@@ -100,17 +115,13 @@ module PotatoMesh
#
# @return [Integer] byte ceiling for HTTP request bodies.
def default_max_json_body_bytes
1_048_576
DEFAULT_MAX_JSON_BODY_BYTES
end
# Determine the maximum allowed JSON body size with validation.
#
# @return [Integer] configured byte limit.
def max_json_body_bytes
raw = ENV.fetch("MAX_JSON_BODY_BYTES", default_max_json_body_bytes.to_s)
value = Integer(raw, 10)
value.positive? ? value : default_max_json_body_bytes
rescue ArgumentError
default_max_json_body_bytes
end
@@ -125,17 +136,13 @@ module PotatoMesh
#
# @return [Integer] refresh period in seconds.
def default_refresh_interval_seconds
60
DEFAULT_REFRESH_INTERVAL_SECONDS
end
# Fetch the refresh interval, ensuring a positive integer value.
#
# @return [Integer] polling cadence in seconds.
def refresh_interval_seconds
raw = ENV.fetch("REFRESH_INTERVAL_SECONDS", default_refresh_interval_seconds.to_s)
value = Integer(raw, 10)
value.positive? ? value : default_refresh_interval_seconds
rescue ArgumentError
default_refresh_interval_seconds
end
@@ -143,20 +150,14 @@ module PotatoMesh
#
# @return [String] CSS filter string.
def map_tile_filter_light
ENV.fetch(
"MAP_TILE_FILTER_LIGHT",
"grayscale(1) saturate(0) brightness(0.92) contrast(1.05)",
)
DEFAULT_TILE_FILTER_LIGHT
end
# Retrieve the CSS filter used for dark themed maps.
#
# @return [String] CSS filter string for dark tiles.
def map_tile_filter_dark
ENV.fetch(
"MAP_TILE_FILTER_DARK",
"grayscale(1) invert(1) brightness(0.9) contrast(1.08)",
)
DEFAULT_TILE_FILTER_DARK
end
# Provide a simple hash of tile filters for template use.
@@ -173,7 +174,7 @@ module PotatoMesh
#
# @return [String] comma separated list of report IDs.
def prom_report_ids
ENV.fetch("PROM_REPORT_IDS", "")
""
end
# Transform Prometheus report identifiers into a cleaned array.
@@ -313,44 +314,85 @@ module PotatoMesh
# Retrieve the default radio channel label.
#
# @return [String] channel name from configuration.
def default_channel
fetch_string("DEFAULT_CHANNEL", "#LongFast")
def channel
fetch_string("CHANNEL", DEFAULT_CHANNEL)
end
# Retrieve the default radio frequency description.
#
# @return [String] frequency identifier.
def default_frequency
fetch_string("DEFAULT_FREQUENCY", "915MHz")
def frequency
fetch_string("FREQUENCY", DEFAULT_FREQUENCY)
end
# Parse the configured map centre coordinates.
#
# @return [Hash{Symbol=>Float}] latitude and longitude in decimal degrees.
def map_center
raw = fetch_string("MAP_CENTER", DEFAULT_MAP_CENTER)
lat_str, lon_str = raw.split(",", 2).map { |part| part&.strip }.compact
lat = Float(lat_str, exception: false)
lon = Float(lon_str, exception: false)
lat = DEFAULT_MAP_CENTER_LAT unless lat
lon = DEFAULT_MAP_CENTER_LON unless lon
{ lat: lat, lon: lon }
end
# Map display latitude centre for the frontend map widget.
#
# @return [Float] latitude in decimal degrees.
def map_center_lat
ENV.fetch("MAP_CENTER_LAT", "38.761944").to_f
map_center[:lat]
end
# Map display longitude centre for the frontend map widget.
#
# @return [Float] longitude in decimal degrees.
def map_center_lon
ENV.fetch("MAP_CENTER_LON", "-27.090833").to_f
map_center[:lon]
end
# Maximum straight-line distance between nodes before relationships are
# hidden.
#
# @return [Float] distance in kilometres.
def max_node_distance_km
ENV.fetch("MAX_NODE_DISTANCE_KM", "42").to_f
def max_distance_km
raw = fetch_string("MAX_DISTANCE", nil)
parsed = raw && Float(raw, exception: false)
return parsed if parsed && parsed.positive?
DEFAULT_MAX_DISTANCE_KM
end
# Matrix room identifier for community discussion.
# Contact link for community discussion.
#
# @return [String] Matrix room alias.
def matrix_room
ENV.fetch("MATRIX_ROOM", "#potatomesh:dod.ngo")
# @return [String] contact URI or identifier.
def contact_link
fetch_string("CONTACT_LINK", DEFAULT_CONTACT_LINK)
end
# Determine the best URL to represent the configured contact link.
#
# @return [String, nil] absolute URL when derivable, otherwise nil.
def contact_link_url
link = contact_link.to_s.strip
return nil if link.empty?
if matrix_alias?(link)
"https://matrix.to/#/#{link}"
elsif link.match?(%r{\Ahttps?://}i)
link
else
nil
end
end
# Check whether a contact link is a Matrix room alias.
#
# @param link [String] candidate link string.
# @return [Boolean] true when the link resembles a Matrix alias.
def matrix_alias?(link)
link.match?(/\A[#!][^\s:]+:[^\s]+\z/)
end
# Check whether verbose debugging is enabled for the runtime.
+4 -4
View File
@@ -34,9 +34,9 @@ module PotatoMesh
# @return [String] generated description text.
def description(private_mode:)
site = Sanitizer.sanitized_site_name
channel = Sanitizer.sanitized_default_channel
frequency = Sanitizer.sanitized_default_frequency
matrix = Sanitizer.sanitized_matrix_room
channel = Sanitizer.sanitized_channel
frequency = Sanitizer.sanitized_frequency
contact = Sanitizer.sanitized_contact_link
summary = "Live Meshtastic mesh map for #{site}"
if channel.empty? && frequency.empty?
@@ -59,7 +59,7 @@ module PotatoMesh
if (distance = Sanitizer.sanitized_max_distance_km)
sentences << "Shows nodes within roughly #{formatted_distance_km(distance)} km of the map center."
end
sentences << "Join the community in #{matrix} on Matrix." if matrix
sentences << "Join the community in #{contact} via chat." if contact
sentences.join(" ")
end
+18 -11
View File
@@ -106,33 +106,40 @@ module PotatoMesh
sanitized_string(Config.site_name)
end
# Retrieve the configured default channel as a cleaned string.
# Retrieve the configured channel as a cleaned string.
#
# @return [String] trimmed configuration value.
def sanitized_default_channel
sanitized_string(Config.default_channel)
def sanitized_channel
sanitized_string(Config.channel)
end
# Retrieve the configured default frequency as a cleaned string.
# Retrieve the configured frequency as a cleaned string.
#
# @return [String] trimmed configuration value.
def sanitized_default_frequency
sanitized_string(Config.default_frequency)
def sanitized_frequency
sanitized_string(Config.frequency)
end
# Retrieve the configured Matrix room and normalise blank values to nil.
# Retrieve the configured contact link and normalise blank values to nil.
#
# @return [String, nil] Matrix room identifier or +nil+ when blank.
def sanitized_matrix_room
value = sanitized_string(Config.matrix_room)
# @return [String, nil] contact link identifier or +nil+ when blank.
def sanitized_contact_link
value = sanitized_string(Config.contact_link)
value.empty? ? nil : value
end
# Retrieve the best effort URL for the configured contact link.
#
# @return [String, nil] contact hyperlink when derivable.
def sanitized_contact_link_url
Config.contact_link_url
end
# Return a positive numeric maximum distance when configured.
#
# @return [Numeric, nil] distance value in kilometres.
def sanitized_max_distance_km
distance = Config.max_node_distance_km
distance = Config.max_distance_km
return nil unless distance.is_a?(Numeric)
return nil unless distance.positive?
@@ -80,9 +80,11 @@ test('mergeConfig coerces numeric values and nested objects', () => {
mapCenter: { lat: '10.5', lon: '20.1' },
tileFilters: { dark: 'contrast(2)' },
chatEnabled: 0,
defaultChannel: '#Custom',
defaultFrequency: '915MHz',
maxNodeDistanceKm: '55.5'
channel: '#Custom',
frequency: '915MHz',
contactLink: 'https://example.org/chat',
contactLinkUrl: 'https://example.org/chat',
maxDistanceKm: '55.5'
});
assert.equal(result.refreshIntervalSeconds, 30);
@@ -90,21 +92,23 @@ test('mergeConfig coerces numeric values and nested objects', () => {
assert.deepEqual(result.mapCenter, { lat: 10.5, lon: 20.1 });
assert.deepEqual(result.tileFilters, { light: DEFAULT_CONFIG.tileFilters.light, dark: 'contrast(2)' });
assert.equal(result.chatEnabled, false);
assert.equal(result.defaultChannel, '#Custom');
assert.equal(result.defaultFrequency, '915MHz');
assert.equal(result.maxNodeDistanceKm, 55.5);
assert.equal(result.channel, '#Custom');
assert.equal(result.frequency, '915MHz');
assert.equal(result.contactLink, 'https://example.org/chat');
assert.equal(result.contactLinkUrl, 'https://example.org/chat');
assert.equal(result.maxDistanceKm, 55.5);
});
test('mergeConfig falls back to defaults for invalid numeric values', () => {
const result = mergeConfig({
refreshIntervalSeconds: 'NaN',
refreshMs: 'NaN',
maxNodeDistanceKm: 'oops'
maxDistanceKm: 'oops'
});
assert.equal(result.refreshIntervalSeconds, DEFAULT_CONFIG.refreshIntervalSeconds);
assert.equal(result.refreshMs, DEFAULT_CONFIG.refreshMs);
assert.equal(result.maxNodeDistanceKm, DEFAULT_CONFIG.maxNodeDistanceKm);
assert.equal(result.maxDistanceKm, DEFAULT_CONFIG.maxDistanceKm);
});
test('document stub returns null for unrelated selectors', () => {
+20 -13
View File
@@ -27,10 +27,10 @@ import { refreshNodeInformation } from './node-details.js';
* refreshMs: number,
* refreshIntervalSeconds: number,
* chatEnabled: boolean,
* defaultChannel: string,
* defaultFrequency: string,
* channel: string,
* frequency: string,
* mapCenter: { lat: number, lon: number },
* maxNodeDistanceKm: number,
* maxDistanceKm: number,
* tileFilters: { light: string, dark: string }
* }} config Normalized application configuration.
* @returns {void}
@@ -111,7 +111,7 @@ export function initializeApp(config) {
const CHAT_RECENT_WINDOW_SECONDS = 7 * 24 * 60 * 60;
const REFRESH_MS = config.refreshMs;
const CHAT_ENABLED = Boolean(config.chatEnabled);
refreshInfo.textContent = `${config.defaultChannel} (${config.defaultFrequency}) — active nodes: …`;
refreshInfo.textContent = `${config.channel} (${config.frequency}) — active nodes: …`;
/** @type {ReturnType<typeof setTimeout>|null} */
let refreshTimer = null;
@@ -285,9 +285,10 @@ export function initializeApp(config) {
let tiles = null;
let offlineTiles = null;
let usingOfflineTiles = false;
const MAX_NODE_DISTANCE_KM = Number.isFinite(config.maxNodeDistanceKm) && config.maxNodeDistanceKm > 0
? config.maxNodeDistanceKm
: 1;
const MAX_DISTANCE_KM = Number.isFinite(config.maxDistanceKm) && config.maxDistanceKm > 0
? config.maxDistanceKm
: null;
const LIMIT_DISTANCE = Number.isFinite(MAX_DISTANCE_KM);
const INITIAL_VIEW_PADDING_PX = 12;
const AUTO_FIT_PADDING_PX = 12;
const MAX_INITIAL_ZOOM = 13;
@@ -1052,7 +1053,11 @@ export function initializeApp(config) {
tiles.addTo(map);
observeTileContainer(tiles);
const initialBounds = computeBoundingBox(MAP_CENTER_COORDS, MAX_NODE_DISTANCE_KM, { minimumRangeKm: 1 });
const initialBounds = computeBoundingBox(
MAP_CENTER_COORDS,
LIMIT_DISTANCE ? MAX_DISTANCE_KM : null,
{ minimumRangeKm: 1 }
);
if (initialBounds) {
fitMapToBounds(initialBounds, { animate: false, paddingPx: INITIAL_VIEW_PADDING_PX, maxZoom: MAX_INITIAL_ZOOM });
} else if (mapCenterLatLng) {
@@ -2726,8 +2731,8 @@ export function initializeApp(config) {
if (!Number.isFinite(srcLat) || !Number.isFinite(srcLon) || !Number.isFinite(tgtLat) || !Number.isFinite(tgtLon)) {
continue;
}
if (sourceNode.distance_km != null && sourceNode.distance_km > MAX_NODE_DISTANCE_KM) continue;
if (targetNode.distance_km != null && targetNode.distance_km > MAX_NODE_DISTANCE_KM) continue;
if (LIMIT_DISTANCE && sourceNode.distance_km != null && sourceNode.distance_km > MAX_DISTANCE_KM) continue;
if (LIMIT_DISTANCE && targetNode.distance_km != null && targetNode.distance_km > MAX_DISTANCE_KM) continue;
const priority = getRoleRenderPriority(sourceNode.role);
const rxTimeRaw = entry.rx_time;
@@ -2820,7 +2825,7 @@ export function initializeApp(config) {
if (latRaw == null || latRaw === '' || lonRaw == null || lonRaw === '') continue;
const lat = Number(latRaw), lon = Number(lonRaw);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
if (n.distance_km != null && n.distance_km > MAX_NODE_DISTANCE_KM) continue;
if (LIMIT_DISTANCE && n.distance_km != null && n.distance_km > MAX_DISTANCE_KM) continue;
const color = getRoleColor(n.role);
const marker = L.circleMarker([lat, lon], {
@@ -2894,7 +2899,9 @@ export function initializeApp(config) {
if (pts.length && fitBoundsEl && fitBoundsEl.checked) {
const bounds = computeBoundsForPoints(pts, {
paddingFraction: 0.2,
minimumRangeKm: Math.min(Math.max(MAX_NODE_DISTANCE_KM * 0.1, 1), MAX_NODE_DISTANCE_KM)
minimumRangeKm: LIMIT_DISTANCE
? Math.min(Math.max(MAX_DISTANCE_KM * 0.1, 1), MAX_DISTANCE_KM)
: 1
});
fitMapToBounds(bounds, { animate: false, paddingPx: AUTO_FIT_PADDING_PX });
}
@@ -3075,6 +3082,6 @@ export function initializeApp(config) {
const c = nodes.filter(n => n.last_heard && nowSec - Number(n.last_heard) <= w.secs).length;
return `${c}/${w.label}`;
}).join(', ');
refreshInfo.textContent = `${config.defaultChannel} (${config.defaultFrequency}) — active nodes: ${counts}.`;
refreshInfo.textContent = `${config.channel} (${config.frequency}) — active nodes: ${counts}.`;
}
}
+17 -11
View File
@@ -19,10 +19,12 @@
* refreshMs: number,
* refreshIntervalSeconds: number,
* chatEnabled: boolean,
* defaultChannel: string,
* defaultFrequency: string,
* channel: string,
* frequency: string,
* contactLink: string,
* contactLinkUrl: string | null,
* mapCenter: { lat: number, lon: number },
* maxNodeDistanceKm: number,
* maxDistanceKm: number,
* tileFilters: { light: string, dark: string }
* }}
*/
@@ -30,10 +32,12 @@ export const DEFAULT_CONFIG = {
refreshMs: 60_000,
refreshIntervalSeconds: 60,
chatEnabled: true,
defaultChannel: '#LongFast',
defaultFrequency: '915MHz',
channel: '#LongFast',
frequency: '915MHz',
contactLink: '#potatomesh:dod.ngo',
contactLinkUrl: 'https://matrix.to/#/#potatomesh:dod.ngo',
mapCenter: { lat: 38.761944, lon: -27.090833 },
maxNodeDistanceKm: 42,
maxDistanceKm: 42,
tileFilters: {
light: 'grayscale(1) saturate(0) brightness(0.92) contrast(1.05)',
dark: 'grayscale(1) invert(1) brightness(0.9) contrast(1.08)'
@@ -65,11 +69,13 @@ export function mergeConfig(raw) {
const refreshMs = Number(raw?.refreshMs ?? config.refreshIntervalSeconds * 1000);
config.refreshMs = Number.isFinite(refreshMs) ? refreshMs : DEFAULT_CONFIG.refreshMs;
config.chatEnabled = Boolean(raw?.chatEnabled ?? DEFAULT_CONFIG.chatEnabled);
config.defaultChannel = raw?.defaultChannel || DEFAULT_CONFIG.defaultChannel;
config.defaultFrequency = raw?.defaultFrequency || DEFAULT_CONFIG.defaultFrequency;
const maxDistance = Number(raw?.maxNodeDistanceKm ?? DEFAULT_CONFIG.maxNodeDistanceKm);
config.maxNodeDistanceKm = Number.isFinite(maxDistance)
config.channel = raw?.channel || DEFAULT_CONFIG.channel;
config.frequency = raw?.frequency || DEFAULT_CONFIG.frequency;
config.contactLink = raw?.contactLink || DEFAULT_CONFIG.contactLink;
config.contactLinkUrl = raw?.contactLinkUrl ?? DEFAULT_CONFIG.contactLinkUrl;
const maxDistance = Number(raw?.maxDistanceKm ?? DEFAULT_CONFIG.maxDistanceKm);
config.maxDistanceKm = Number.isFinite(maxDistance)
? maxDistance
: DEFAULT_CONFIG.maxNodeDistanceKm;
: DEFAULT_CONFIG.maxDistanceKm;
return config;
}
+18 -37
View File
@@ -26,37 +26,18 @@ RSpec.describe "Potato Mesh Sinatra app" do
let(:application_class) { PotatoMesh::Application }
describe "configuration" do
it "sets the default HTTP port to 41_447" do
expect(app.settings.port).to eq(41_447)
it "sets the default HTTP port to the baked-in value" do
expect(app.settings.port).to eq(PotatoMesh::Application::DEFAULT_PORT)
end
end
describe ".resolve_port" do
around do |example|
original_present = ENV.key?("PORT")
original_value = ENV["PORT"] if original_present
ENV.delete("PORT")
example.run
ensure
if original_present
ENV["PORT"] = original_value
else
ENV.delete("PORT")
end
end
it "returns the default port when the environment is unset" do
expect(application_class.resolve_port).to eq(41_447)
end
it "parses the environment override when provided" do
it "always returns the baked-in default port" do
expect(application_class.resolve_port).to eq(PotatoMesh::Application::DEFAULT_PORT)
ENV["PORT"] = "51515"
expect(application_class.resolve_port).to eq(51_515)
end
it "falls back to the default port when parsing fails" do
ENV["PORT"] = "potato"
expect(application_class.resolve_port).to eq(41_447)
expect(application_class.resolve_port).to eq(PotatoMesh::Application::DEFAULT_PORT)
ensure
ENV.delete("PORT")
end
end
@@ -495,9 +476,9 @@ RSpec.describe "Potato Mesh Sinatra app" do
expect(sanitized_string(nil)).to eq("")
end
it "returns nil for blank matrix rooms" do
allow(PotatoMesh::Config).to receive(:matrix_room).and_return(" \t ")
expect(sanitized_matrix_room).to be_nil
it "returns nil for blank contact links" do
allow(PotatoMesh::Config).to receive(:contact_link).and_return(" \t ")
expect(sanitized_contact_link).to be_nil
end
it "coerces string_or_nil inputs" do
@@ -566,13 +547,13 @@ RSpec.describe "Potato Mesh Sinatra app" do
end
it "returns nil when the maximum distance is invalid" do
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return(-5)
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return(-5)
expect(sanitized_max_distance_km).to be_nil
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return("string")
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return("string")
expect(sanitized_max_distance_km).to be_nil
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return(15.5)
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return(15.5)
expect(sanitized_max_distance_km).to eq(15.5)
end
end
@@ -785,12 +766,12 @@ RSpec.describe "Potato Mesh Sinatra app" do
it "includes SEO metadata from configuration" do
allow(PotatoMesh::Config).to receive(:site_name).and_return("Spec Mesh Title")
allow(PotatoMesh::Config).to receive(:default_channel).and_return("#SpecChannel")
allow(PotatoMesh::Config).to receive(:default_frequency).and_return("915MHz")
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return(120.5)
allow(PotatoMesh::Config).to receive(:matrix_room).and_return(" #spec-room:example.org ")
allow(PotatoMesh::Config).to receive(:channel).and_return("#SpecChannel")
allow(PotatoMesh::Config).to receive(:frequency).and_return("915MHz")
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return(120.5)
allow(PotatoMesh::Config).to receive(:contact_link).and_return(" #spec-room:example.org ")
expected_description = "Live Meshtastic mesh map for Spec Mesh Title on #SpecChannel (915MHz). Track nodes, messages, and coverage in real time. Shows nodes within roughly 120.5 km of the map center. Join the community in #spec-room:example.org on Matrix."
expected_description = "Live Meshtastic mesh map for Spec Mesh Title on #SpecChannel (915MHz). Track nodes, messages, and coverage in real time. Shows nodes within roughly 120.5 km of the map center. Join the community in #spec-room:example.org via chat."
get "/"
+104 -59
View File
@@ -138,77 +138,122 @@ RSpec.describe PotatoMesh::Config do
end
describe ".db_path" do
it "uses the environment override when available" do
within_env("MESH_DB" => "/tmp/spec.db") do
expect(described_class.db_path).to eq("/tmp/spec.db")
end
end
it "falls back to the bundled database path" do
within_env("MESH_DB" => nil) do
expect(described_class.db_path).to eq(described_class.default_db_path)
end
it "returns the default path inside the data directory" do
expect(described_class.db_path).to eq(described_class.default_db_path)
expect(described_class.db_path).to eq(File.join(described_class.data_directory, "mesh.db"))
end
end
describe ".max_json_body_bytes" do
it "returns the default when the value is missing" do
within_env("MAX_JSON_BODY_BYTES" => nil) do
expect(described_class.max_json_body_bytes).to eq(
described_class.default_max_json_body_bytes,
)
end
end
it "returns the parsed integer when valid" do
within_env("MAX_JSON_BODY_BYTES" => "2048") do
expect(described_class.max_json_body_bytes).to eq(2048)
end
end
it "rejects invalid and non-positive values" do
within_env("MAX_JSON_BODY_BYTES" => "potato") do
expect(described_class.max_json_body_bytes).to eq(
described_class.default_max_json_body_bytes,
)
end
within_env("MAX_JSON_BODY_BYTES" => "0") do
expect(described_class.max_json_body_bytes).to eq(
described_class.default_max_json_body_bytes,
)
end
it "returns the baked-in default size" do
expect(described_class.max_json_body_bytes).to eq(described_class.default_max_json_body_bytes)
end
end
describe ".refresh_interval_seconds" do
it "returns the default when the configuration is invalid" do
within_env("REFRESH_INTERVAL_SECONDS" => "invalid") do
expect(described_class.refresh_interval_seconds).to eq(
described_class.default_refresh_interval_seconds,
)
end
end
it "honours positive integer overrides" do
within_env("REFRESH_INTERVAL_SECONDS" => "120") do
expect(described_class.refresh_interval_seconds).to eq(120)
end
end
it "rejects zero or negative overrides" do
within_env("REFRESH_INTERVAL_SECONDS" => "0") do
expect(described_class.refresh_interval_seconds).to eq(
described_class.default_refresh_interval_seconds,
)
end
it "returns the baked-in refresh cadence" do
expect(described_class.refresh_interval_seconds).to eq(described_class.default_refresh_interval_seconds)
end
end
describe ".prom_report_id_list" do
it "splits and normalises identifiers" do
within_env("PROM_REPORT_IDS" => " alpha , beta,, ") do
expect(described_class.prom_report_id_list).to eq(%w[alpha beta])
it "returns an empty collection when no identifiers are configured" do
expect(described_class.prom_report_id_list).to eq([])
end
end
describe ".channel" do
it "returns the default channel when unset" do
within_env("CHANNEL" => nil) do
expect(described_class.channel).to eq(PotatoMesh::Config::DEFAULT_CHANNEL)
end
end
it "trims whitespace from overrides" do
within_env("CHANNEL" => " #Spec ") do
expect(described_class.channel).to eq("#Spec")
end
end
end
describe ".frequency" do
it "returns the default frequency when unset" do
within_env("FREQUENCY" => nil) do
expect(described_class.frequency).to eq(PotatoMesh::Config::DEFAULT_FREQUENCY)
end
end
it "trims whitespace from overrides" do
within_env("FREQUENCY" => " 915MHz ") do
expect(described_class.frequency).to eq("915MHz")
end
end
end
describe ".map_center" do
it "parses latitude and longitude from the environment" do
within_env("MAP_CENTER" => "10.5, -20.25") do
expect(described_class.map_center).to eq({ lat: 10.5, lon: -20.25 })
end
end
it "falls back to defaults when parsing fails" do
within_env("MAP_CENTER" => "potato") do
expect(described_class.map_center).to eq({ lat: PotatoMesh::Config::DEFAULT_MAP_CENTER_LAT, lon: PotatoMesh::Config::DEFAULT_MAP_CENTER_LON })
end
end
end
describe ".max_distance_km" do
it "returns the default distance when unset" do
within_env("MAX_DISTANCE" => nil) do
expect(described_class.max_distance_km).to eq(PotatoMesh::Config::DEFAULT_MAX_DISTANCE_KM)
end
end
it "parses positive numeric overrides" do
within_env("MAX_DISTANCE" => "105.5") do
expect(described_class.max_distance_km).to eq(105.5)
end
end
it "rejects invalid overrides" do
within_env("MAX_DISTANCE" => "-1") do
expect(described_class.max_distance_km).to eq(PotatoMesh::Config::DEFAULT_MAX_DISTANCE_KM)
end
end
end
describe ".contact_link" do
it "returns the default contact when unset" do
within_env("CONTACT_LINK" => nil) do
expect(described_class.contact_link).to eq(PotatoMesh::Config::DEFAULT_CONTACT_LINK)
end
end
it "trims whitespace from overrides" do
within_env("CONTACT_LINK" => " https://example.org/chat ") do
expect(described_class.contact_link).to eq("https://example.org/chat")
end
end
end
describe ".contact_link_url" do
it "builds a matrix.to URL for aliases" do
within_env("CONTACT_LINK" => "#spec:example.org") do
expect(described_class.contact_link_url).to eq("https://matrix.to/#/#spec:example.org")
end
end
it "passes through existing URLs" do
within_env("CONTACT_LINK" => "https://example.org/chat") do
expect(described_class.contact_link_url).to eq("https://example.org/chat")
end
end
it "returns nil for unrecognised values" do
within_env("CONTACT_LINK" => "Community Portal") do
expect(described_class.contact_link_url).to be_nil
end
end
end
+14 -12
View File
@@ -62,35 +62,37 @@ RSpec.describe PotatoMesh::Sanitizer do
before do
allow(PotatoMesh::Config).to receive_messages(
site_name: " Spec Mesh ",
default_channel: " #Spec ",
default_frequency: " 915MHz ",
matrix_room: " #room:example.org ",
max_node_distance_km: 42,
channel: " #Spec ",
frequency: " 915MHz ",
contact_link: " #room:example.org ",
max_distance_km: 42,
)
end
it "provides trimmed strings" do
expect(described_class.sanitized_site_name).to eq("Spec Mesh")
expect(described_class.sanitized_default_channel).to eq("#Spec")
expect(described_class.sanitized_default_frequency).to eq("915MHz")
expect(described_class.sanitized_matrix_room).to eq("#room:example.org")
expect(described_class.sanitized_channel).to eq("#Spec")
expect(described_class.sanitized_frequency).to eq("915MHz")
expect(described_class.sanitized_contact_link).to eq("#room:example.org")
expect(described_class.sanitized_contact_link_url).to eq("https://matrix.to/#/#room:example.org")
expect(described_class.sanitized_max_distance_km).to eq(42)
end
it "returns nil when the matrix room is blank" do
allow(PotatoMesh::Config).to receive(:matrix_room).and_return(" \t ")
it "returns nil when the contact link is blank" do
allow(PotatoMesh::Config).to receive(:contact_link).and_return(" \t ")
expect(described_class.sanitized_matrix_room).to be_nil
expect(described_class.sanitized_contact_link).to be_nil
expect(described_class.sanitized_contact_link_url).to be_nil
end
it "returns nil when the distance is not positive" do
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return(0)
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return(0)
expect(described_class.sanitized_max_distance_km).to be_nil
end
it "returns nil when the distance is not numeric" do
allow(PotatoMesh::Config).to receive(:max_node_distance_km).and_return("far")
allow(PotatoMesh::Config).to receive(:max_distance_km).and_return("far")
expect(described_class.sanitized_max_distance_km).to be_nil
end
-1
View File
@@ -36,7 +36,6 @@ require "fileutils"
ENV["RACK_ENV"] = "test"
SPEC_TMPDIR = Dir.mktmpdir("potato-mesh-spec-")
ENV["MESH_DB"] = File.join(SPEC_TMPDIR, "mesh.db")
ENV["XDG_DATA_HOME"] = File.join(SPEC_TMPDIR, "xdg-data")
ENV["XDG_CONFIG_HOME"] = File.join(SPEC_TMPDIR, "xdg-config")
+19 -11
View File
@@ -83,7 +83,7 @@
<div class="row meta">
<div class="meta-info">
<div class="refresh-row">
<p id="refreshInfo" class="refresh-info" aria-live="polite"><%= default_channel %> (<%= default_frequency %>) — active nodes: …</p>
<p id="refreshInfo" class="refresh-info" aria-live="polite"><%= channel %> (<%= frequency %>) — active nodes: …</p>
<div class="refresh-actions">
<label class="auto-refresh-toggle"><input type="checkbox" id="autoRefresh" checked /> Auto-refresh every <%= refresh_interval_seconds %> seconds</label>
<button id="refreshBtn" type="button">Refresh now</button>
@@ -108,19 +108,23 @@
<h2 id="infoTitle" class="info-title">About <%= site_name %></h2>
<p class="info-intro">Quick facts about this PotatoMesh instance.</p>
<dl class="info-details">
<dt>Default channel</dt>
<dd><%= default_channel %></dd>
<dt>Channel</dt>
<dd><%= channel %></dd>
<dt>Frequency</dt>
<dd><%= default_frequency %></dd>
<dd><%= frequency %></dd>
<dt>Map center</dt>
<dd><%= format("%.5f, %.5f", map_center_lat, map_center_lon) %></dd>
<dt>Visible range</dt>
<dd>Nodes within roughly <%= max_node_distance_km %> km of the center are shown.</dd>
<dd>Nodes within roughly <%= max_distance_km %> km of the center are shown.</dd>
<dt>Auto-refresh</dt>
<dd>Updates every <%= refresh_interval_seconds %> seconds.</dd>
<% if matrix_room && !matrix_room.empty? %>
<dt>Matrix room</dt>
<dd><a href="https://matrix.to/#/<%= matrix_room %>" target="_blank" rel="noreferrer noopener"><%= matrix_room %></a></dd>
<% if contact_link && !contact_link.empty? %>
<dt>Chat</dt>
<% if contact_link_url %>
<dd><a href="<%= contact_link_url %>" target="_blank" rel="noreferrer noopener"><%= contact_link %></a></dd>
<% else %>
<dd><%= contact_link %></dd>
<% end %>
<% end %>
</dl>
</div>
@@ -196,9 +200,13 @@
<span class="mono"><%= version %></span> —
<% end %>
GitHub: <a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
<% if matrix_room && !matrix_room.empty? %>
— <%= site_name %> Matrix:
<a href="https://matrix.to/#/<%= matrix_room %>" target="_blank"><%= matrix_room %></a>
<% if contact_link && !contact_link.empty? %>
— <%= site_name %> chat:
<% if contact_link_url %>
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
<% else %>
<%= contact_link %>
<% end %>
<% end %>
</footer>