diff --git a/AGENTS.md b/AGENTS.md index dafcd79..762f36c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -455,7 +455,7 @@ See [PLAN.md](PLAN.md#configuration-environment-variables) for complete list. Key variables: - `DATA_HOME` - Base directory for runtime data (default: `./data`) - `SEED_HOME` - Directory containing seed data files (default: `./seed`) -- `PAGES_HOME` - Directory containing custom markdown pages (default: `./pages`) +- `CONTENT_HOME` - Directory containing custom content (pages, media) (default: `./content`) - `MQTT_HOST`, `MQTT_PORT`, `MQTT_PREFIX` - MQTT broker connection - `MQTT_TLS` - Enable TLS/SSL for MQTT (default: `false`) - `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys @@ -473,12 +473,16 @@ ${SEED_HOME}/ └── members.yaml # Network members list ``` -**Custom Pages (`PAGES_HOME`)** - Contains custom markdown pages for the web dashboard: +**Custom Content (`CONTENT_HOME`)** - Contains custom pages and media for the web dashboard: ``` -${PAGES_HOME}/ -├── about.md # Example: About page (/pages/about) -├── faq.md # Example: FAQ page (/pages/faq) -└── getting-started.md # Example: Getting Started (/pages/getting-started) +${CONTENT_HOME}/ +├── pages/ # Custom markdown pages +│ ├── about.md # Example: About page (/pages/about) +│ ├── faq.md # Example: FAQ page (/pages/faq) +│ └── getting-started.md # Example: Getting Started (/pages/getting-started) +└── media/ # Custom media files + └── images/ + └── logo.svg # Custom logo (replaces default favicon and navbar/home logo) ``` Pages use YAML frontmatter for metadata: diff --git a/README.md b/README.md index 594d306..560be11 100644 --- a/README.md +++ b/README.md @@ -338,19 +338,28 @@ The collector automatically cleans up old event data and inactive nodes: | `NETWORK_CONTACT_EMAIL` | *(none)* | Contact email address | | `NETWORK_CONTACT_DISCORD` | *(none)* | Discord server link | | `NETWORK_CONTACT_GITHUB` | *(none)* | GitHub repository URL | -| `PAGES_HOME` | `./pages` | Directory containing custom markdown pages | +| `CONTENT_HOME` | `./content` | Directory containing custom content (pages/, media/) | -### Custom Pages +### Custom Content -The web dashboard supports custom markdown pages for adding static content like "About Us", "Getting Started", or "FAQ" pages. Pages are stored as markdown files with YAML frontmatter. +The web dashboard supports custom content including markdown pages and media files. Content is organized in subdirectories: + +``` +content/ +├── pages/ # Custom markdown pages +│ └── about.md +└── media/ # Custom media files + └── images/ + └── logo.svg # Custom logo (replaces favicon and navbar/home logo) +``` **Setup:** ```bash -# Create pages directory -mkdir -p pages +# Create content directory structure +mkdir -p content/pages content/media # Create a custom page -cat > pages/about.md << 'EOF' +cat > content/pages/about.md << 'EOF' --- title: About Us slug: about @@ -378,14 +387,14 @@ EOF The markdown content is rendered as-is, so include your own `# Heading` if desired. -Pages automatically appear in the navigation menu and sitemap. With Docker, mount the pages directory: +Pages automatically appear in the navigation menu and sitemap. With Docker, mount the content directory: ```yaml # docker-compose.yml (already configured) volumes: - - ${PAGES_HOME:-./pages}:/pages:ro + - ${CONTENT_HOME:-./content}:/content:ro environment: - - PAGES_HOME=/pages + - CONTENT_HOME=/content ``` ## Seed Data @@ -594,10 +603,16 @@ meshcore-hub/ │ ├── seed/ # Example seed data files │ │ ├── node_tags.yaml # Example node tags │ │ └── members.yaml # Example network members -│ └── pages/ # Example custom pages -│ └── about.md # Example about page +│ └── content/ # Example custom content +│ ├── pages/ # Example custom pages +│ │ └── about.md # Example about page +│ └── media/ # Example media files +│ └── images/ # Custom images ├── seed/ # Seed data directory (SEED_HOME, copy from example/seed/) -├── pages/ # Custom pages directory (PAGES_HOME, optional) +├── content/ # Custom content directory (CONTENT_HOME, optional) +│ ├── pages/ # Custom markdown pages +│ └── media/ # Custom media files +│ └── images/ # Custom images (logo.svg replaces default logo) ├── data/ # Runtime data directory (DATA_HOME, created at runtime) ├── Dockerfile # Docker build configuration ├── docker-compose.yml # Docker Compose services diff --git a/docker-compose.yml b/docker-compose.yml index 88c7e30..7eaf78b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -242,7 +242,7 @@ services: ports: - "${WEB_PORT:-8080}:8080" volumes: - - ${PAGES_HOME:-./pages}:/pages:ro + - ${CONTENT_HOME:-./content}:/content:ro environment: - LOG_LEVEL=${LOG_LEVEL:-INFO} - API_BASE_URL=http://api:8000 @@ -260,7 +260,7 @@ services: - NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-} - NETWORK_CONTACT_GITHUB=${NETWORK_CONTACT_GITHUB:-} - NETWORK_WELCOME_TEXT=${NETWORK_WELCOME_TEXT:-} - - PAGES_HOME=/pages + - CONTENT_HOME=/content command: ["web"] healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"] diff --git a/example/content/media/images/logo_ipnet.svg b/example/content/media/images/logo_ipnet.svg new file mode 100644 index 0000000..3bf6bf4 --- /dev/null +++ b/example/content/media/images/logo_ipnet.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + diff --git a/example/pages/join.md b/example/content/pages/join.md similarity index 100% rename from example/pages/join.md rename to example/content/pages/join.md diff --git a/src/meshcore_hub/common/config.py b/src/meshcore_hub/common/config.py index 9022ca9..bf1b919 100644 --- a/src/meshcore_hub/common/config.py +++ b/src/meshcore_hub/common/config.py @@ -295,18 +295,32 @@ class WebSettings(CommonSettings): default=None, description="Welcome text for homepage" ) - # Custom pages directory - pages_home: Optional[str] = Field( + # Content directory (contains pages/ and media/ subdirectories) + content_home: Optional[str] = Field( default=None, - description="Directory containing custom markdown pages (default: ./pages)", + description="Directory containing custom content (pages/, media/) (default: ./content)", ) @property - def effective_pages_home(self) -> str: - """Get the effective pages home directory.""" + def effective_content_home(self) -> str: + """Get the effective content home directory.""" from pathlib import Path - return str(Path(self.pages_home or "./pages")) + return str(Path(self.content_home or "./content")) + + @property + def effective_pages_home(self) -> str: + """Get the effective pages directory (content_home/pages).""" + from pathlib import Path + + return str(Path(self.effective_content_home) / "pages") + + @property + def effective_media_home(self) -> str: + """Get the effective media directory (content_home/media).""" + from pathlib import Path + + return str(Path(self.effective_content_home) / "media") @property def web_data_dir(self) -> str: diff --git a/src/meshcore_hub/web/app.py b/src/meshcore_hub/web/app.py index f3e9d0f..258c544 100644 --- a/src/meshcore_hub/web/app.py +++ b/src/meshcore_hub/web/app.py @@ -132,10 +132,23 @@ def create_app( page_loader.load_pages() app.state.page_loader = page_loader + # Check for custom logo and store media path + media_home = Path(settings.effective_media_home) + custom_logo_path = media_home / "images" / "logo.svg" + if custom_logo_path.exists(): + app.state.logo_url = "/media/images/logo.svg" + logger.info(f"Using custom logo from {custom_logo_path}") + else: + app.state.logo_url = "/static/img/logo.svg" + # Mount static files if STATIC_DIR.exists(): app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # Mount custom media files if directory exists + if media_home.exists() and media_home.is_dir(): + app.mount("/media", StaticFiles(directory=str(media_home)), name="media") + # Include routers from meshcore_hub.web.routes import web_router @@ -292,5 +305,6 @@ def get_network_context(request: Request) -> dict: "network_welcome_text": request.app.state.network_welcome_text, "admin_enabled": request.app.state.admin_enabled, "custom_pages": custom_pages, + "logo_url": request.app.state.logo_url, "version": __version__, } diff --git a/src/meshcore_hub/web/static/img/logo.svg b/src/meshcore_hub/web/static/img/logo.svg index 3bf6bf4..1709a2b 100644 --- a/src/meshcore_hub/web/static/img/logo.svg +++ b/src/meshcore_hub/web/static/img/logo.svg @@ -1,61 +1,21 @@ - - - - - - - + xmlns="http://www.w3.org/2000/svg"> + - - - + stroke-width="8" + stroke-linecap="round"> + + + + + + diff --git a/src/meshcore_hub/web/templates/base.html b/src/meshcore_hub/web/templates/base.html index 8136eb0..b508ac9 100644 --- a/src/meshcore_hub/web/templates/base.html +++ b/src/meshcore_hub/web/templates/base.html @@ -25,7 +25,7 @@ - + @@ -112,7 +112,7 @@ - {{ network_name }} + {{ network_name }} {{ network_name }} diff --git a/src/meshcore_hub/web/templates/home.html b/src/meshcore_hub/web/templates/home.html index b02e89e..1e6a8b1 100644 --- a/src/meshcore_hub/web/templates/home.html +++ b/src/meshcore_hub/web/templates/home.html @@ -10,7 +10,7 @@
- {{ network_name }} + {{ network_name }}

{{ network_name }}

{% if network_city and network_country %} diff --git a/tests/test_web/test_pages.py b/tests/test_web/test_pages.py index ba8b799..2bd69ff 100644 --- a/tests/test_web/test_pages.py +++ b/tests/test_web/test_pages.py @@ -346,9 +346,12 @@ class TestPagesRoute: @pytest.fixture def pages_dir(self) -> Generator[str, None, None]: - """Create a temporary directory with test pages.""" + """Create a temporary content directory with test pages.""" with tempfile.TemporaryDirectory() as tmpdir: - (Path(tmpdir) / "about.md").write_text( + # Create pages subdirectory (CONTENT_HOME/pages) + pages_subdir = Path(tmpdir) / "pages" + pages_subdir.mkdir() + (pages_subdir / "about.md").write_text( """--- title: About Us slug: about @@ -360,7 +363,7 @@ menu_order: 10 Welcome to the network. """ ) - (Path(tmpdir) / "faq.md").write_text( + (pages_subdir / "faq.md").write_text( """--- title: FAQ slug: faq @@ -381,8 +384,8 @@ Here are some answers. """Create a web app with custom pages configured.""" import os - # Temporarily set PAGES_HOME environment variable - os.environ["PAGES_HOME"] = pages_dir + # Temporarily set CONTENT_HOME environment variable + os.environ["CONTENT_HOME"] = pages_dir from meshcore_hub.web.app import create_app @@ -396,7 +399,7 @@ Here are some answers. yield app # Cleanup - del os.environ["PAGES_HOME"] + del os.environ["CONTENT_HOME"] @pytest.fixture def client_with_pages( @@ -442,9 +445,12 @@ class TestPagesInSitemap: @pytest.fixture def pages_dir(self) -> Generator[str, None, None]: - """Create a temporary directory with test pages.""" + """Create a temporary content directory with test pages.""" with tempfile.TemporaryDirectory() as tmpdir: - (Path(tmpdir) / "about.md").write_text( + # Create pages subdirectory (CONTENT_HOME/pages) + pages_subdir = Path(tmpdir) / "pages" + pages_subdir.mkdir() + (pages_subdir / "about.md").write_text( """--- title: About slug: about @@ -462,7 +468,7 @@ About page. """Create a test client with custom pages for sitemap testing.""" import os - os.environ["PAGES_HOME"] = pages_dir + os.environ["CONTENT_HOME"] = pages_dir from meshcore_hub.web.app import create_app @@ -476,7 +482,7 @@ About page. client = TestClient(app, raise_server_exceptions=True) yield client - del os.environ["PAGES_HOME"] + del os.environ["CONTENT_HOME"] def test_pages_included_in_sitemap( self, client_with_pages_for_sitemap: TestClient