Compare commits

1 Commits

Author SHA1 Message Date
Louis King
db86b3198e Some minor UI improvements, updated env.example, and docs 2025-12-08 23:06:04 +00:00
6 changed files with 266 additions and 180 deletions

View File

@@ -1,17 +1,40 @@
# MeshCore Hub - Docker Compose Environment Configuration
# MeshCore Hub - Environment Configuration
# Copy this file to .env and customize values
#
# Configuration is grouped by service. Most deployments only need:
# - Common Settings (always required)
# - MQTT Settings (always required)
# - Interface Settings (for receiver/sender services)
#
# The Collector, API, and Web services typically run as a combined "core"
# profile and share the same data directory.
#
# -----------------------------------------------------------------------------
# QUICK START: Receiver/Sender Only
# -----------------------------------------------------------------------------
# For a minimal receiver or sender setup, you only need these settings:
#
# MQTT_HOST=your-mqtt-broker.example.com
# MQTT_PORT=1883
# MQTT_USERNAME=your_username
# MQTT_PASSWORD=your_password
# MQTT_TLS=false
# SERIAL_PORT=/dev/ttyUSB0
#
# Serial ports are typically /dev/ttyUSB[0-9] or /dev/ttyACM[0-9] on Linux.
# -----------------------------------------------------------------------------
# ===================
# Docker Image
# ===================
# =============================================================================
# COMMON SETTINGS
# =============================================================================
# These settings apply to all services
# Docker image version tag to use
# Options: latest, main, v1.0.0, etc.
IMAGE_VERSION=latest
# ===================
# Data & Seed Directories
# ===================
# Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOG_LEVEL=INFO
# Base directory for runtime data (database, etc.)
# Default: ./data (relative to docker-compose.yml location)
@@ -19,7 +42,8 @@ IMAGE_VERSION=latest
#
# Structure:
# ${DATA_HOME}/
# └── meshcore.db # SQLite database
# └── collector/
# └── meshcore.db # SQLite database
DATA_HOME=./data
# Directory containing seed data files for import
@@ -32,43 +56,43 @@ DATA_HOME=./data
# └── members.yaml # Network members for import
SEED_HOME=./seed
# ===================
# Common Settings
# ===================
# =============================================================================
# MQTT SETTINGS
# =============================================================================
# MQTT broker connection settings for interface, collector, and API services
# Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOG_LEVEL=INFO
# ===================
# MQTT Settings
# ===================
# MQTT Broker connection (for interface/collector/api services)
# When using the local MQTT broker (--profile mqtt), use "mqtt" as host
# MQTT Broker host
# When using the local MQTT broker (--profile mqtt), use "mqtt"
# When using an external broker, set the hostname/IP
MQTT_HOST=mqtt
# MQTT Broker port (default: 1883, or 8883 for TLS)
MQTT_PORT=1883
# MQTT authentication (optional)
MQTT_USERNAME=
MQTT_PASSWORD=
# MQTT topic prefix for all MeshCore messages
MQTT_PREFIX=meshcore
# Enable TLS/SSL for MQTT connection (default: false)
# Enable TLS/SSL for MQTT connection
# When enabled, uses TLS with system CA certificates (e.g., for Let's Encrypt)
# Set to true for secure MQTT connections (port 8883)
MQTT_TLS=false
# External port mappings for local MQTT broker (--profile mqtt only)
MQTT_EXTERNAL_PORT=1883
MQTT_WS_PORT=9001
# ===================
# Interface Settings
# ===================
# =============================================================================
# INTERFACE SETTINGS (Receiver/Sender)
# =============================================================================
# Settings for the MeshCore device interface services
# Serial port for receiver device
SERIAL_PORT=/dev/ttyUSB0
# Serial port for sender device (if separate)
# Serial port for sender device (if using separate device)
SERIAL_PORT_SENDER=/dev/ttyUSB1
# Baud rate for serial communication
@@ -83,55 +107,21 @@ MESHCORE_DEVICE_NAME=
NODE_ADDRESS=
NODE_ADDRESS_SENDER=
# ===================
# API Settings
# ===================
# =============================================================================
# COLLECTOR SETTINGS
# =============================================================================
# The collector subscribes to MQTT events and stores them in the database
# External API port
API_PORT=8000
# API Keys for authentication (generate secure keys for production!)
# Example: openssl rand -hex 32
API_READ_KEY=
API_ADMIN_KEY=
# ===================
# Web Dashboard Settings
# ===================
# External web port
WEB_PORT=8080
# Network Information (displayed on web dashboard)
NETWORK_NAME=MeshCore Network
NETWORK_CITY=
NETWORK_COUNTRY=
# Radio configuration (comma-delimited)
# Format: <profile>,<frequency>,<bandwidth>,<spreading_factor>,<coding_rate>,<tx_power>
# Example: EU/UK Narrow,869.618MHz,62.5kHz,8,8,22dBm
NETWORK_RADIO_CONFIG=
# Contact information
NETWORK_CONTACT_EMAIL=
NETWORK_CONTACT_DISCORD=
NETWORK_CONTACT_GITHUB=
# Welcome text displayed on the homepage (plain text, optional)
# If not set, a default welcome message is shown
NETWORK_WELCOME_TEXT=
# ===================
# -------------------
# Webhook Settings
# ===================
# -------------------
# Webhooks forward mesh events to external HTTP endpoints as POST requests
# Webhook for advertisement events (node discovery)
# Events are sent as POST requests with JSON payload
WEBHOOK_ADVERTISEMENT_URL=
WEBHOOK_ADVERTISEMENT_SECRET=
# Webhook for all message events (channel and direct messages)
# Use this for a single endpoint handling all messages
WEBHOOK_MESSAGE_URL=
WEBHOOK_MESSAGE_SECRET=
@@ -147,34 +137,82 @@ WEBHOOK_TIMEOUT=10.0
WEBHOOK_MAX_RETRIES=3
WEBHOOK_RETRY_BACKOFF=2.0
# ===================
# -------------------
# Data Retention Settings
# ===================
# -------------------
# Automatic cleanup of old event data (advertisements, messages, telemetry, etc.)
# Enable automatic cleanup of old event data
# When enabled, the collector runs periodic cleanup to delete old events
# Default: true
DATA_RETENTION_ENABLED=true
# Number of days to retain event data (advertisements, messages, telemetry, etc.)
# Number of days to retain event data
# Events older than this are deleted during cleanup
# Default: 30 days
DATA_RETENTION_DAYS=30
# Hours between automatic cleanup runs (applies to both events and nodes)
# Default: 24 hours (once per day)
# Hours between automatic cleanup runs
# Applies to both event data and node cleanup
DATA_RETENTION_INTERVAL_HOURS=24
# ===================
# -------------------
# Node Cleanup Settings
# ===================
# -------------------
# Automatic removal of inactive nodes
# Enable automatic cleanup of inactive nodes
# Nodes that haven't been seen (last_seen) for the specified period are removed
# Nodes with last_seen=NULL (never seen on network) are NOT removed
# Default: true
NODE_CLEANUP_ENABLED=true
# Remove nodes not seen for this many days (based on last_seen field)
# Default: 7 days
NODE_CLEANUP_DAYS=7
# =============================================================================
# API SETTINGS
# =============================================================================
# REST API for querying data and sending commands
# External API port
API_PORT=8000
# API Keys for authentication
# Generate secure keys for production: openssl rand -hex 32
# Leave empty to disable authentication (not recommended for production)
API_READ_KEY=
API_ADMIN_KEY=
# =============================================================================
# WEB DASHBOARD SETTINGS
# =============================================================================
# Web interface for visualizing network status
# External web port
WEB_PORT=8080
# -------------------
# Network Information
# -------------------
# Displayed on the web dashboard homepage
# Network display name
NETWORK_NAME=MeshCore Network
# Network location
NETWORK_CITY=
NETWORK_COUNTRY=
# Radio configuration (comma-delimited)
# Format: <profile>,<frequency>,<bandwidth>,<spreading_factor>,<coding_rate>,<tx_power>
# Example: EU/UK Narrow,869.618MHz,62.5kHz,SF8,CR8,22dBm
NETWORK_RADIO_CONFIG=
# Welcome text displayed on the homepage (optional, plain text)
# If not set, a default welcome message is shown
NETWORK_WELCOME_TEXT=
# -------------------
# Contact Information
# -------------------
# Contact links displayed in the footer
NETWORK_CONTACT_EMAIL=
NETWORK_CONTACT_DISCORD=
NETWORK_CONTACT_GITHUB=

View File

@@ -274,10 +274,9 @@ meshcore-hub/
│ │ ├── app.py # FastAPI app
│ │ ├── auth.py # Authentication
│ │ ├── dependencies.py
│ │ ── routes/ # API routes
│ │ ├── members.py # Member CRUD endpoints
│ │ └── ...
│ │ └── templates/ # Dashboard HTML
│ │ ── routes/ # API routes
│ │ ├── members.py # Member CRUD endpoints
│ │ └── ...
│ └── web/
│ ├── cli.py
│ ├── app.py # FastAPI app
@@ -457,10 +456,12 @@ Key variables:
- `DATA_HOME` - Base directory for runtime data (default: `./data`)
- `SEED_HOME` - Directory containing seed data files (default: `./seed`)
- `MQTT_HOST`, `MQTT_PORT`, `MQTT_PREFIX` - MQTT broker connection
- `DATABASE_URL` - SQLAlchemy database URL (default: `sqlite:///{DATA_HOME}/collector/meshcore.db`)
- `MQTT_TLS` - Enable TLS/SSL for MQTT (default: `false`)
- `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys
- `LOG_LEVEL` - Logging verbosity
The database defaults to `sqlite:///{DATA_HOME}/collector/meshcore.db` and does not typically need to be configured.
### Directory Structure
**Seed Data (`SEED_HOME`)** - Contains initial data files for database seeding:
@@ -479,13 +480,21 @@ ${DATA_HOME}/
Services automatically create their subdirectories if they don't exist.
### Automatic Seeding
### Seeding
The collector automatically imports seed data on startup if YAML files exist in `SEED_HOME`:
The database can be seeded with node tags and network members from YAML files in `SEED_HOME`:
- `node_tags.yaml` - Node tag definitions (keyed by public_key)
- `members.yaml` - Network member definitions
Manual seeding can be triggered with: `meshcore-hub collector seed`
Seeding is a separate process from the collector and must be run explicitly:
```bash
# Native CLI
meshcore-hub collector seed
# With Docker Compose
docker compose --profile seed up
```
### Webhook Configuration
@@ -559,9 +568,9 @@ Webhook payload structure:
### Common Issues
1. **MQTT Connection Failed**: Check broker is running and `MQTT_HOST`/`MQTT_PORT` are correct
2. **Database Migration Errors**: Ensure `DATABASE_URL` is correct, run `alembic upgrade head`
2. **Database Migration Errors**: Ensure `DATA_HOME` is writable, run `meshcore-hub db upgrade`
3. **Import Errors**: Ensure package is installed with `pip install -e .`
4. **Type Errors**: Run `mypy src/` to check type annotations
4. **Type Errors**: Run `pre-commit run --all-files` to check type annotations and other issues
### Debugging

View File

@@ -297,26 +297,26 @@ All components are configured via environment variables. Create a `.env` file or
| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |
| `DATA_HOME` | `./data` | Base directory for runtime data |
| `SEED_HOME` | `./seed` | Directory containing seed data files |
| `MQTT_HOST` | `localhost` | MQTT broker hostname |
| `MQTT_PORT` | `1883` | MQTT broker port |
| `MQTT_USERNAME` | *(none)* | MQTT username (optional) |
| `MQTT_PASSWORD` | *(none)* | MQTT password (optional) |
| `MQTT_PREFIX` | `meshcore` | Topic prefix for all MQTT messages |
| `MQTT_TLS` | `false` | Enable TLS/SSL for MQTT connection |
### Interface Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `INTERFACE_MODE` | `RECEIVER` | Operating mode (RECEIVER or SENDER) |
| `SERIAL_PORT` | `/dev/ttyUSB0` | Serial port for MeshCore device |
| `SERIAL_BAUD` | `115200` | Serial baud rate |
| `MESHCORE_DEVICE_NAME` | *(none)* | Device/node name set on startup (broadcast in advertisements) |
| `MOCK_DEVICE` | `false` | Use mock device for testing |
### Collector Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `sqlite:///{data_home}/collector/meshcore.db` | SQLAlchemy database URL |
| `SEED_HOME` | `./seed` | Directory containing seed data files (node_tags.yaml, members.yaml) |
The database is stored in `{DATA_HOME}/collector/meshcore.db` by default.
#### Webhook Configuration
@@ -343,6 +343,18 @@ Webhook payload format:
}
```
#### Data Retention
The collector automatically cleans up old event data and inactive nodes:
| Variable | Default | Description |
|----------|---------|-------------|
| `DATA_RETENTION_ENABLED` | `true` | Enable automatic cleanup of old events |
| `DATA_RETENTION_DAYS` | `30` | Days to retain event data |
| `DATA_RETENTION_INTERVAL_HOURS` | `24` | Hours between cleanup runs |
| `NODE_CLEANUP_ENABLED` | `true` | Enable removal of inactive nodes |
| `NODE_CLEANUP_DAYS` | `7` | Remove nodes not seen for this many days |
### API Settings
| Variable | Default | Description |
@@ -362,6 +374,11 @@ Webhook payload format:
| `NETWORK_NAME` | `MeshCore Network` | Display name for the network |
| `NETWORK_CITY` | *(none)* | City where network is located |
| `NETWORK_COUNTRY` | *(none)* | Country code (ISO 3166-1 alpha-2) |
| `NETWORK_RADIO_CONFIG` | *(none)* | Radio config (comma-delimited: profile,freq,bw,sf,cr,power) |
| `NETWORK_WELCOME_TEXT` | *(none)* | Custom welcome text for homepage |
| `NETWORK_CONTACT_EMAIL` | *(none)* | Contact email address |
| `NETWORK_CONTACT_DISCORD` | *(none)* | Discord server link |
| `NETWORK_CONTACT_GITHUB` | *(none)* | GitHub repository URL |
## CLI Reference
@@ -375,7 +392,7 @@ meshcore-hub interface --mode receiver --device-name "Gateway Node" # Set devic
meshcore-hub interface --mode sender --mock # Use mock device
# Collector component
meshcore-hub collector # Run collector (auto-seeds on startup)
meshcore-hub collector # Run collector
meshcore-hub collector seed # Import all seed data from SEED_HOME
meshcore-hub collector import-tags # Import node tags from SEED_HOME/node_tags.yaml
meshcore-hub collector import-tags /path/to/file.yaml # Import from specific file
@@ -396,15 +413,11 @@ meshcore-hub db current # Show current revision
## Seed Data
The collector supports seeding the database with node tags and network members on startup. Seed files are read from the `SEED_HOME` directory (default: `./seed`).
The database can be seeded with node tags and network members from YAML files in the `SEED_HOME` directory (default: `./seed`).
### Automatic Seeding
### Running the Seed Process
When the collector starts, it automatically imports seed data from YAML files if they exist:
- `{SEED_HOME}/node_tags.yaml` - Node tag definitions
- `{SEED_HOME}/members.yaml` - Network member definitions
### Manual Seeding
Seeding is a separate process and must be run explicitly:
```bash
# Native CLI
@@ -414,6 +427,10 @@ meshcore-hub collector seed
docker compose --profile seed up
```
This imports data from the following files (if they exist):
- `{SEED_HOME}/node_tags.yaml` - Node tag definitions
- `{SEED_HOME}/members.yaml` - Network member definitions
### Directory Structure
```
@@ -484,17 +501,21 @@ Network members represent the people operating nodes in your network. Members ca
### Members YAML Format
```yaml
members:
- name: John Doe
callsign: N0CALL
role: Network Operator
description: Example member entry
contact: john@example.com
public_key: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- member_id: walshie86
name: Walshie
callsign: Walshie86
role: member
description: IPNet Member
- member_id: craig
name: Craig
callsign: M7XCN
role: member
description: IPNet Member
```
| Field | Required | Description |
|-------|----------|-------------|
| `member_id` | Yes | Unique identifier for the member |
| `name` | Yes | Member's display name |
| `callsign` | No | Amateur radio callsign |
| `role` | No | Member's role in the network |
@@ -620,14 +641,8 @@ pytest -k "test_list"
### Code Quality
```bash
# Format code
black src/ tests/
# Lint
flake8 src/ tests/
# Type check
mypy src/
# Run all code quality checks (formatting, linting, type checking)
pre-commit run --all-files
```
### Creating Database Migrations
@@ -684,7 +699,7 @@ meshcore-hub/
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests and linting (`pytest && black . && flake8`)
4. Run tests and quality checks (`pytest && pre-commit run --all-files`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request

View File

@@ -29,8 +29,9 @@ async def home(request: Request) -> HTMLResponse:
"advertisements_24h": 0,
}
# Fetch activity data for chart
activity = {"days": 7, "data": []}
# Fetch activity data for charts
advert_activity = {"days": 7, "data": []}
message_activity = {"days": 7, "data": []}
try:
response = await request.app.state.http_client.get("/api/v1/dashboard/stats")
@@ -45,12 +46,22 @@ async def home(request: Request) -> HTMLResponse:
"/api/v1/dashboard/activity", params={"days": 7}
)
if response.status_code == 200:
activity = response.json()
advert_activity = response.json()
except Exception as e:
logger.warning(f"Failed to fetch activity from API: {e}")
try:
response = await request.app.state.http_client.get(
"/api/v1/dashboard/message-activity", params={"days": 7}
)
if response.status_code == 200:
message_activity = response.json()
except Exception as e:
logger.warning(f"Failed to fetch message activity from API: {e}")
context["stats"] = stats
# Pass activity data as JSON string for the chart
context["activity_json"] = json.dumps(activity)
# Pass activity data as JSON strings for the chart
context["advert_activity_json"] = json.dumps(advert_activity)
context["message_activity_json"] = json.dumps(message_activity)
return templates.TemplateResponse("home.html", context)

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 134 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M3.277,0.053C2.829,0.053 2.401,0.41 2.321,0.851L0.013,13.623C-0.067,14.064 0.232,14.421 0.681,14.421L3.13,14.421C3.578,14.421 4.006,14.064 4.086,13.623L5.004,8.54L6.684,13.957C6.766,14.239 7.02,14.421 7.337,14.421L10.58,14.421C10.897,14.421 11.217,14.239 11.401,13.957L15.043,8.513L14.119,13.623C14.038,14.064 14.338,14.421 14.787,14.421L17.236,14.421C17.684,14.421 18.112,14.064 18.192,13.623L20.5,0.851C20.582,0.41 20.283,0.053 19.834,0.053L16.69,0.053C16.373,0.053 16.053,0.235 15.87,0.517L9.897,9.473C9.803,9.616 9.578,9.578 9.528,9.41L7.074,0.517C6.992,0.235 6.738,0.053 6.421,0.053L3.277,0.053Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M21.146,14.421C21.146,14.421 33.257,14.421 33.257,14.421C33.526,14.421 33.784,14.205 33.831,13.942L34.337,11.128C34.385,10.863 34.206,10.649 33.936,10.649L25.519,10.649C25.429,10.649 25.37,10.576 25.385,10.488L25.635,9.105C25.65,9.017 25.736,8.944 25.826,8.944L32.596,8.944C32.865,8.944 33.123,8.728 33.171,8.465L33.621,5.974C33.669,5.709 33.49,5.495 33.221,5.495L26.45,5.495C26.361,5.495 26.301,5.423 26.317,5.335L26.584,3.852C26.599,3.764 26.685,3.691 26.775,3.691L35.192,3.691C35.462,3.691 35.719,3.476 35.767,3.21L36.258,0.498C36.306,0.235 36.126,0.019 35.857,0.019L23.746,0.019C23.297,0.019 22.867,0.378 22.788,0.819L20.474,13.621C20.396,14.062 20.695,14.421 21.146,14.421Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M45.926,14.419L45.926,14.421L46.346,14.421C48.453,14.421 50.465,12.742 50.839,10.67L51.081,9.327C51.456,7.256 50.05,5.576 47.943,5.576L41.455,5.576C41.186,5.576 41.007,5.363 41.054,5.097L41.218,4.192C41.266,3.927 41.524,3.713 41.793,3.713L50.569,3.713C51.018,3.713 51.446,3.356 51.526,2.915L51.9,0.85C51.98,0.407 51.68,0.05 51.232,0.05L41.638,0.05C39.531,0.05 37.519,1.73 37.145,3.801L36.88,5.267C36.505,7.339 37.91,9.018 40.018,9.018L46.506,9.018C46.775,9.018 46.954,9.231 46.907,9.497L46.785,10.176C46.737,10.441 46.479,10.655 46.21,10.655L37.189,10.655C36.741,10.655 36.313,11.012 36.233,11.453L35.841,13.621C35.761,14.062 36.061,14.419 36.51,14.419L45.926,14.419Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M68.008,0.046C68.008,0.046 65.296,0.046 65.296,0.046C64.847,0.046 64.42,0.403 64.34,0.844L63.532,5.31C63.517,5.398 63.431,5.469 63.341,5.469L58.085,5.469C57.995,5.469 57.936,5.398 57.951,5.31L58.758,0.844C58.837,0.403 58.539,0.046 58.09,0.046L55.378,0.046C54.93,0.046 54.502,0.403 54.422,0.844L52.112,13.623C52.032,14.064 52.331,14.421 52.78,14.421L55.492,14.421C55.941,14.421 56.369,14.064 56.449,13.623L57.272,9.074C57.287,8.986 57.373,8.914 57.462,8.914L62.719,8.914C62.809,8.914 62.868,8.985 62.853,9.074L62.032,13.623C61.952,14.064 62.252,14.421 62.7,14.421L65.413,14.421C65.861,14.421 66.289,14.064 66.369,13.623L68.678,0.844C68.755,0.403 68.457,0.046 68.008,0.046Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M72.099,14.421C72.099,14.421 80.066,14.421 80.066,14.421C80.515,14.421 80.943,14.064 81.022,13.623L81.414,11.453C81.494,11.012 81.194,10.655 80.746,10.655L73.828,10.655C73.559,10.655 73.38,10.441 73.427,10.176L74.51,4.215C74.558,3.951 74.815,3.736 75.082,3.736L82,3.736C82.448,3.736 82.876,3.379 82.956,2.938L83.34,0.817C83.42,0.376 83.12,0.019 82.672,0.019L74.724,0.019C72.622,0.019 70.614,1.691 70.236,3.757L68.965,10.665C68.587,12.738 69.99,14.421 72.099,14.421Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M97.176,-0C97.176,0 88.882,0 88.882,0C86.775,0 84.763,1.68 84.389,3.751L83.139,10.67C82.765,12.741 84.169,14.421 86.277,14.421L94.571,14.421C96.678,14.421 98.69,12.741 99.064,10.67L100.314,3.751C100.689,1.68 99.284,-0 97.176,-0ZM94.798,10.178C94.75,10.443 94.492,10.657 94.223,10.657L87.978,10.657C87.709,10.657 87.529,10.443 87.577,10.178L88.659,4.192C88.707,3.927 88.964,3.713 89.234,3.713L95.477,3.713C95.747,3.713 95.926,3.927 95.878,4.192L94.798,10.178Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M101.284,14.421L103.995,14.421C104.443,14.421 104.871,14.065 104.951,13.624L105.43,10.97C105.446,10.882 105.531,10.81 105.621,10.81L108.902,10.806C109.064,10.806 109.2,10.886 109.267,11.018L110.813,14.035C110.992,14.392 111.319,14.434 112.303,14.419C112.88,14.426 113.756,14.382 115.169,14.382C115.623,14.382 115.902,13.907 115.678,13.51L113.989,10.569C113.945,10.491 113.993,10.386 114.086,10.34C115.39,9.707 116.423,8.477 116.681,7.055L117.27,3.785C117.646,1.713 116.242,0.033 114.134,0.033L103.884,0.033C103.436,0.033 103.008,0.39 102.928,0.831L100.616,13.623C100.536,14.064 100.836,14.421 101.284,14.421L101.284,14.421ZM106.73,3.791C106.745,3.703 106.831,3.631 106.921,3.631L112.225,3.631C112.626,3.631 112.891,3.949 112.821,4.343L112.431,6.494C112.359,6.885 111.979,7.204 111.58,7.204L106.276,7.204C106.186,7.204 106.127,7.133 106.142,7.043L106.73,3.791Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M118.277,14.421C118.277,14.421 130.388,14.421 130.388,14.421C130.657,14.421 130.915,14.205 130.963,13.942L131.468,11.128C131.516,10.863 131.337,10.649 131.068,10.649L122.65,10.649C122.56,10.649 122.501,10.576 122.516,10.488L122.766,9.105C122.781,9.017 122.867,8.944 122.957,8.944L129.728,8.944C129.997,8.944 130.254,8.728 130.302,8.465L130.753,5.974C130.801,5.709 130.621,5.495 130.352,5.495L123.581,5.495C123.492,5.495 123.432,5.423 123.448,5.335L123.715,3.852C123.73,3.764 123.816,3.691 123.906,3.691L132.324,3.691C132.593,3.691 132.851,3.476 132.898,3.21L133.389,0.498C133.437,0.235 133.257,0.019 132.988,0.019L120.877,0.019C120.428,0.019 119.999,0.378 119.919,0.819L117.605,13.621C117.527,14.062 117.827,14.421 118.277,14.421Z" style="fill:white;fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -111,18 +111,18 @@
<span class="font-mono">{{ network_radio_config.frequency }}</span>
</div>
{% endif %}
{% if network_radio_config.spreading_factor %}
<div class="flex justify-between">
<span class="opacity-70">Spreading Factor:</span>
<span class="font-mono">{{ network_radio_config.spreading_factor }}</span>
</div>
{% endif %}
{% if network_radio_config.bandwidth %}
<div class="flex justify-between">
<span class="opacity-70">Bandwidth:</span>
<span class="font-mono">{{ network_radio_config.bandwidth }}</span>
</div>
{% endif %}
{% if network_radio_config.spreading_factor %}
<div class="flex justify-between">
<span class="opacity-70">Spreading Factor:</span>
<span class="font-mono">{{ network_radio_config.spreading_factor }}</span>
</div>
{% endif %}
{% if network_radio_config.coding_rate %}
<div class="flex justify-between">
<span class="opacity-70">Coding Rate:</span>
@@ -146,6 +146,31 @@
</div>
</div>
<!-- Powered by MeshCore -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body flex flex-col items-center justify-center">
<p class="text-sm opacity-70 mb-4 text-center">Our local off-grid mesh network is made possible by</p>
<a href="https://meshcore.co.uk/" target="_blank" rel="noopener noreferrer" class="hover:opacity-80 transition-opacity">
<img src="/static/img/meshcore.svg" alt="MeshCore" class="h-8" />
</a>
<p class="text-xs opacity-50 mt-4 text-center">Connecting people and things, without using the internet</p>
<div class="flex gap-2 mt-4">
<a href="https://meshcore.co.uk/" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Website
</a>
<a href="https://github.com/meshcore-dev/MeshCore" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
GitHub
</a>
</div>
</div>
</div>
<!-- Network Activity Chart -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
@@ -155,53 +180,12 @@
</svg>
Network Activity
</h2>
<p class="text-sm opacity-70 mb-2">Advertisements received per day (last 7 days)</p>
<p class="text-sm opacity-70 mb-2">Activity per day (last 7 days)</p>
<div class="h-48">
<canvas id="activityChart"></canvas>
</div>
</div>
</div>
<!-- Contact Card -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Contact
</h2>
<div class="space-y-2">
{% if network_contact_email %}
<a href="mailto:{{ network_contact_email }}" class="btn btn-outline btn-sm btn-block">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
{{ network_contact_email }}
</a>
{% endif %}
{% if network_contact_discord %}
<a href="{{ network_contact_discord }}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm btn-block">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>
Discord
</a>
{% endif %}
{% if network_contact_github %}
<a href="{{ network_contact_github }}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm btn-block">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
GitHub
</a>
{% endif %}
{% if not network_contact_email and not network_contact_discord and not network_contact_github %}
<p class="text-sm opacity-70">No contact information configured.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
@@ -209,16 +193,18 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
(function() {
const activityData = {{ activity_json | safe }};
const advertData = {{ advert_activity_json | safe }};
const messageData = {{ message_activity_json | safe }};
const ctx = document.getElementById('activityChart');
if (ctx && activityData.data && activityData.data.length > 0) {
if (ctx && advertData.data && advertData.data.length > 0) {
// Format dates for display (show only day/month)
const labels = activityData.data.map(d => {
const labels = advertData.data.map(d => {
const date = new Date(d.date);
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' });
});
const counts = activityData.data.map(d => d.count);
const advertCounts = advertData.data.map(d => d.count);
const messageCounts = messageData.data ? messageData.data.map(d => d.count) : [];
new Chart(ctx, {
type: 'line',
@@ -226,10 +212,19 @@
labels: labels,
datasets: [{
label: 'Advertisements',
data: counts,
data: advertCounts,
borderColor: 'oklch(0.7 0.17 330)',
backgroundColor: 'oklch(0.7 0.17 330 / 0.1)',
fill: true,
fill: false,
tension: 0.3,
pointRadius: 2,
pointHoverRadius: 5
}, {
label: 'Messages',
data: messageCounts,
borderColor: 'oklch(0.7 0.15 200)',
backgroundColor: 'oklch(0.7 0.15 200 / 0.1)',
fill: false,
tension: 0.3,
pointRadius: 2,
pointHoverRadius: 5
@@ -240,7 +235,13 @@
maintainAspectRatio: false,
plugins: {
legend: {
display: false
display: true,
position: 'bottom',
labels: {
color: 'oklch(0.7 0 0)',
boxWidth: 12,
padding: 8
}
},
tooltip: {
mode: 'index',