mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db86b3198e |
198
.env.example
198
.env.example
@@ -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=
|
||||
|
||||
29
AGENTS.md
29
AGENTS.md
@@ -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
|
||||
|
||||
|
||||
75
README.md
75
README.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
src/meshcore_hub/web/static/img/meshcore.svg
Normal file
12
src/meshcore_hub/web/static/img/meshcore.svg
Normal 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 |
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user