This commit is contained in:
Louis King
2025-12-04 18:10:29 +00:00
parent 110c701787
commit cf2c3350cc
7 changed files with 133 additions and 49 deletions
+4
View File
@@ -94,6 +94,10 @@ NETWORK_RADIO_CONFIG=
NETWORK_CONTACT_EMAIL=
NETWORK_CONTACT_DISCORD=
# Welcome text displayed on the homepage (plain text, optional)
# If not set, a default welcome message is shown
NETWORK_WELCOME_TEXT=
# Members file location (optional override)
# Default: ${DATA_HOME}/web/members.json
# Only set this if you want to use a different location
+2
View File
@@ -241,6 +241,8 @@ services:
- NETWORK_RADIO_CONFIG=${NETWORK_RADIO_CONFIG:-}
- NETWORK_CONTACT_EMAIL=${NETWORK_CONTACT_EMAIL:-}
- NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-}
- NETWORK_CONTACT_GITHUB=${NETWORK_CONTACT_GITHUB:-}
- NETWORK_WELCOME_TEXT=${NETWORK_WELCOME_TEXT:-}
command: ["web"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
+6
View File
@@ -243,6 +243,12 @@ class WebSettings(CommonSettings):
network_contact_discord: Optional[str] = Field(
default=None, description="Discord server link"
)
network_contact_github: Optional[str] = Field(
default=None, description="GitHub repository URL"
)
network_welcome_text: Optional[str] = Field(
default=None, description="Welcome text for homepage"
)
@property
def web_data_dir(self) -> str:
+37 -11
View File
@@ -48,18 +48,23 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
def create_app(
api_url: str = "http://localhost:8000",
api_url: str | None = None,
api_key: str | None = None,
network_name: str = "MeshCore Network",
network_name: str | None = None,
network_city: str | None = None,
network_country: str | None = None,
network_location: tuple[float, float] | None = None,
network_radio_config: str | None = None,
network_contact_email: str | None = None,
network_contact_discord: str | None = None,
network_contact_github: str | None = None,
network_welcome_text: str | None = None,
) -> FastAPI:
"""Create and configure the web dashboard application.
When called without arguments (e.g., in reload mode), settings are loaded
from environment variables via the WebSettings class.
Args:
api_url: Base URL of the MeshCore Hub API
api_key: API key for authentication
@@ -70,10 +75,17 @@ def create_app(
network_radio_config: Radio configuration description
network_contact_email: Contact email address
network_contact_discord: Discord invite/server info
network_contact_github: GitHub repository URL
network_welcome_text: Welcome text for homepage
Returns:
Configured FastAPI application
"""
# Load settings from environment if not provided
from meshcore_hub.common.config import get_web_settings
settings = get_web_settings()
app = FastAPI(
title="MeshCore Hub Dashboard",
description="Web dashboard for MeshCore network visualization",
@@ -83,16 +95,28 @@ def create_app(
redoc_url=None,
)
# Store configuration in app state
app.state.api_url = api_url
app.state.api_key = api_key
app.state.network_name = network_name
app.state.network_city = network_city
app.state.network_country = network_country
# Store configuration in app state (use args if provided, else settings)
app.state.api_url = api_url or settings.api_base_url
app.state.api_key = api_key or settings.api_key
app.state.network_name = network_name or settings.network_name
app.state.network_city = network_city or settings.network_city
app.state.network_country = network_country or settings.network_country
app.state.network_location = network_location or (0.0, 0.0)
app.state.network_radio_config = network_radio_config
app.state.network_contact_email = network_contact_email
app.state.network_contact_discord = network_contact_discord
app.state.network_radio_config = (
network_radio_config or settings.network_radio_config
)
app.state.network_contact_email = (
network_contact_email or settings.network_contact_email
)
app.state.network_contact_discord = (
network_contact_discord or settings.network_contact_discord
)
app.state.network_contact_github = (
network_contact_github or settings.network_contact_github
)
app.state.network_welcome_text = (
network_welcome_text or settings.network_welcome_text
)
# Set up templates
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
@@ -148,5 +172,7 @@ def get_network_context(request: Request) -> dict:
"network_radio_config": radio_config,
"network_contact_email": request.app.state.network_contact_email,
"network_contact_discord": request.app.state.network_contact_discord,
"network_contact_github": request.app.state.network_contact_github,
"network_welcome_text": request.app.state.network_welcome_text,
"version": __version__,
}
+63 -33
View File
@@ -7,21 +7,21 @@ import click
@click.option(
"--host",
type=str,
default="0.0.0.0",
default=None,
envvar="WEB_HOST",
help="Web server host",
help="Web server host (default: 0.0.0.0)",
)
@click.option(
"--port",
type=int,
default=8080,
default=None,
envvar="WEB_PORT",
help="Web server port",
help="Web server port (default: 8080)",
)
@click.option(
"--api-url",
type=str,
default="http://localhost:8000",
default=None,
envvar="API_BASE_URL",
help="API server base URL",
)
@@ -42,7 +42,7 @@ import click
@click.option(
"--network-name",
type=str,
default="MeshCore Network",
default=None,
envvar="NETWORK_NAME",
help="Network display name",
)
@@ -63,14 +63,14 @@ import click
@click.option(
"--network-lat",
type=float,
default=0.0,
default=None,
envvar="NETWORK_LAT",
help="Network center latitude",
)
@click.option(
"--network-lon",
type=float,
default=0.0,
default=None,
envvar="NETWORK_LON",
help="Network center longitude",
)
@@ -95,6 +95,20 @@ import click
envvar="NETWORK_CONTACT_DISCORD",
help="Discord server info",
)
@click.option(
"--network-contact-github",
type=str,
default=None,
envvar="NETWORK_CONTACT_GITHUB",
help="GitHub repository URL",
)
@click.option(
"--network-welcome-text",
type=str,
default=None,
envvar="NETWORK_WELCOME_TEXT",
help="Welcome text for homepage",
)
@click.option(
"--reload",
is_flag=True,
@@ -104,19 +118,21 @@ import click
@click.pass_context
def web(
ctx: click.Context,
host: str,
port: int,
api_url: str,
host: str | None,
port: int | None,
api_url: str | None,
api_key: str | None,
data_home: str | None,
network_name: str,
network_name: str | None,
network_city: str | None,
network_country: str | None,
network_lat: float,
network_lon: float,
network_lat: float | None,
network_lon: float | None,
network_radio_config: str | None,
network_contact_email: str | None,
network_contact_discord: str | None,
network_contact_github: str | None,
network_welcome_text: str | None,
reload: bool,
) -> None:
"""Run the web dashboard.
@@ -146,46 +162,58 @@ def web(
from meshcore_hub.common.config import get_web_settings
from meshcore_hub.web.app import create_app
# Get settings to compute effective values
# Get settings for defaults and display
settings = get_web_settings()
# Override data_home if provided
if data_home:
settings = settings.model_copy(update={"data_home": data_home})
# Use CLI args or fall back to settings
effective_host = host or settings.web_host
effective_port = port or settings.web_port
effective_data_home = data_home or settings.data_home
# Ensure web data directory exists
web_data_dir = Path(effective_data_home) / "web"
web_data_dir.mkdir(parents=True, exist_ok=True)
# Display effective settings
effective_network_name = network_name or settings.network_name
click.echo("=" * 50)
click.echo("MeshCore Hub Web Dashboard")
click.echo("=" * 50)
click.echo(f"Host: {host}")
click.echo(f"Port: {port}")
click.echo(f"Host: {effective_host}")
click.echo(f"Port: {effective_port}")
click.echo(f"Data home: {effective_data_home}")
click.echo(f"API URL: {api_url}")
click.echo(f"API key configured: {api_key is not None}")
click.echo(f"Network: {network_name}")
if network_city and network_country:
click.echo(f"Location: {network_city}, {network_country}")
if network_lat != 0.0 or network_lon != 0.0:
click.echo(f"Map center: {network_lat}, {network_lon}")
click.echo(f"API URL: {api_url or settings.api_base_url}")
click.echo(f"API key configured: {(api_key or settings.api_key) is not None}")
click.echo(f"Network: {effective_network_name}")
effective_city = network_city or settings.network_city
effective_country = network_country or settings.network_country
if effective_city and effective_country:
click.echo(f"Location: {effective_city}, {effective_country}")
effective_lat = network_lat if network_lat is not None else 0.0
effective_lon = network_lon if network_lon is not None else 0.0
if effective_lat != 0.0 or effective_lon != 0.0:
click.echo(f"Map center: {effective_lat}, {effective_lon}")
click.echo(f"Reload mode: {reload}")
click.echo("=" * 50)
network_location = (network_lat, network_lon)
# Build network_location tuple only if explicitly provided
network_location: tuple[float, float] | None = None
if network_lat is not None or network_lon is not None:
network_location = (
network_lat if network_lat is not None else 0.0,
network_lon if network_lon is not None else 0.0,
)
if reload:
# For development, use uvicorn's reload feature
click.echo("\nStarting in development mode with auto-reload...")
click.echo("Note: Using default settings for reload mode.")
click.echo("Note: Settings loaded from environment/config.")
uvicorn.run(
"meshcore_hub.web.app:create_app",
host=host,
port=port,
host=effective_host,
port=effective_port,
reload=True,
factory=True,
)
@@ -201,7 +229,9 @@ def web(
network_radio_config=network_radio_config,
network_contact_email=network_contact_email,
network_contact_discord=network_contact_discord,
network_contact_github=network_contact_github,
network_welcome_text=network_welcome_text,
)
click.echo("\nStarting web dashboard...")
uvicorn.run(app, host=host, port=port)
uvicorn.run(app, host=effective_host, port=effective_port)
+5 -1
View File
@@ -105,7 +105,11 @@
{% endif %}
{% if network_contact_email and network_contact_discord %} | {% endif %}
{% if network_contact_discord %}
<span>Discord: {{ network_contact_discord }}</span>
<a href="{{ network_contact_discord }}" target="_blank" rel="noopener noreferrer" class="link link-hover">Discord</a>
{% endif %}
{% if (network_contact_email or network_contact_discord) and network_contact_github %} | {% endif %}
{% if network_contact_github %}
<a href="{{ network_contact_github }}" target="_blank" rel="noopener noreferrer" class="link link-hover">GitHub</a>
{% endif %}
</p>
<p class="text-xs opacity-50 mt-2">Powered by MeshCore Hub v{{ version }}</p>
+16 -4
View File
@@ -10,10 +10,14 @@
{% if network_city and network_country %}
<p class="py-2 text-lg opacity-70">{{ network_city }}, {{ network_country }}</p>
{% endif %}
{% if network_welcome_text %}
<p class="py-6">{{ network_welcome_text }}</p>
{% else %}
<p class="py-6">
Welcome to the {{ network_name }} mesh network dashboard.
Monitor network activity, view connected nodes, and explore message history.
</p>
{% endif %}
<div class="flex gap-4 justify-center flex-wrap">
<a href="/network" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -133,14 +137,22 @@
</a>
{% endif %}
{% if network_contact_discord %}
<div class="btn btn-outline btn-sm btn-block">
<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>
{{ network_contact_discord }}
</div>
Discord
</a>
{% endif %}
{% if not network_contact_email and not network_contact_discord %}
{% 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>