diff --git a/AGENTS.md b/AGENTS.md index 479967e..ea3151b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -678,6 +678,7 @@ The static site uses a modern, responsive design with the following features: - **Repeater pages at root**: `/day.html`, `/week.html`, etc. (entry point) - **Companion pages**: `/companion/day.html`, `/companion/week.html`, etc. - **`.htaccess`**: Sets `DirectoryIndex day.html` so `/` loads repeater day view +- **Relative links**: All internal navigation and static asset references are relative (no leading `/`) so the dashboard can be served from a reverse-proxy subpath. ### Page Layout 1. **Header**: Site branding, node name, pubkey prefix, status indicator, last updated time diff --git a/src/meshmon/html.py b/src/meshmon/html.py index 764a729..4087d62 100644 --- a/src/meshmon/html.py +++ b/src/meshmon/html.py @@ -480,6 +480,7 @@ def build_chart_groups( role: str, period: str, chart_stats: dict | None = None, + asset_prefix: str = "", ) -> list[dict]: """Build chart groups for template. @@ -490,6 +491,7 @@ def build_chart_groups( role: "companion" or "repeater" period: Time period ("day", "week", etc.) chart_stats: Stats dict from chart_stats.json (optional) + asset_prefix: Relative path prefix to reach /assets from page location """ cfg = get_config() groups_config = REPEATER_CHART_GROUPS if role == "repeater" else COMPANION_CHART_GROUPS @@ -551,8 +553,9 @@ def build_chart_groups( chart_data["use_svg"] = True else: # Fallback to PNG paths - chart_data["src_light"] = f"/assets/{role}/{metric}_{period}_light.png" - chart_data["src_dark"] = f"/assets/{role}/{metric}_{period}_dark.png" + asset_base = f"{asset_prefix}assets/{role}/" + chart_data["src_light"] = f"{asset_base}{metric}_{period}_light.png" + chart_data["src_dark"] = f"{asset_base}{metric}_{period}_dark.png" chart_data["use_svg"] = False charts.append(chart_data) @@ -614,7 +617,10 @@ def build_page_context( # Load chart stats and build chart groups chart_stats = load_chart_stats(role) - chart_groups = build_chart_groups(role, period, chart_stats) + + # Relative path prefixes (avoid absolute paths for subpath deployments) + css_path = "" if at_root else "../" + asset_prefix = "" if at_root else "../" # Period config page_title, page_subtitle = PERIOD_CONFIG.get(period, ("Observations", "Radio telemetry")) @@ -634,9 +640,18 @@ def build_page_context( ), } - # CSS and link paths - depend on whether we're at root or in /companion/ - css_path = "/" if at_root else "../" - base_path = "" if at_root else "/companion" + chart_groups = build_chart_groups(role, period, chart_stats, asset_prefix=asset_prefix) + + # Navigation links depend on whether we're at root or in /companion/ + base_path = "" + if at_root: + repeater_link = "day.html" + companion_link = "companion/day.html" + reports_link = "reports/" + else: + repeater_link = "../day.html" + companion_link = "day.html" + reports_link = "../reports/" return { # Page meta @@ -665,9 +680,9 @@ def build_page_context( # Navigation "period": period, "base_path": base_path, - "repeater_link": f"{css_path}day.html", - "companion_link": f"{css_path}companion/day.html", - "reports_link": f"{css_path}reports/", + "repeater_link": repeater_link, + "companion_link": companion_link, + "reports_link": reports_link, # Timestamps "last_updated": last_updated, diff --git a/src/meshmon/templates/node.html b/src/meshmon/templates/node.html index 3c80017..94ffba1 100644 --- a/src/meshmon/templates/node.html +++ b/src/meshmon/templates/node.html @@ -113,10 +113,10 @@
- Day - Week - Month - Year + Day + Week + Month + Year
diff --git a/tests/html/test_page_context.py b/tests/html/test_page_context.py index 212b0ca..fee365b 100644 --- a/tests/html/test_page_context.py +++ b/tests/html/test_page_context.py @@ -229,5 +229,28 @@ class TestBuildPageContext: at_root=False, ) - assert root_context["css_path"] == "/" + assert root_context["css_path"] == "" assert non_root_context["css_path"] == "../" + + def test_links_use_relative_paths(self, configured_env, sample_row): + """Navigation and asset links are relative for subpath deployments.""" + root_context = build_page_context( + role="repeater", + period="day", + row=sample_row, + at_root=True, + ) + non_root_context = build_page_context( + role="companion", + period="day", + row=sample_row, + at_root=False, + ) + + assert root_context["repeater_link"] == "day.html" + assert root_context["companion_link"] == "companion/day.html" + assert root_context["reports_link"] == "reports/" + + assert non_root_context["repeater_link"] == "../day.html" + assert non_root_context["companion_link"] == "day.html" + assert non_root_context["reports_link"] == "../reports/" diff --git a/tests/html/test_write_site.py b/tests/html/test_write_site.py index b4533a1..2f94439 100644 --- a/tests/html/test_write_site.py +++ b/tests/html/test_write_site.py @@ -266,7 +266,8 @@ class TestHtmlOutput: content = (out_dir / "day.html").read_text() - assert "styles.css" in content + assert 'href="styles.css"' in content + assert 'href="/styles.css"' not in content def test_companion_pages_relative_css(self, html_env, metrics_rows): """Companion pages use relative path to CSS.""" @@ -277,4 +278,5 @@ class TestHtmlOutput: content = (out_dir / "companion" / "day.html").read_text() # Should reference parent directory CSS - assert "../styles.css" in content or "styles.css" in content + assert 'href="../styles.css"' in content + assert 'href="/styles.css"' not in content diff --git a/tests/unit/test_html_builders.py b/tests/unit/test_html_builders.py index 22fa885..3be052f 100644 --- a/tests/unit/test_html_builders.py +++ b/tests/unit/test_html_builders.py @@ -6,6 +6,7 @@ from meshmon.html import ( PERIOD_CONFIG, REPEATER_CHART_GROUPS, _build_traffic_table_rows, + build_chart_groups, build_companion_metrics, build_node_details, build_radio_config, @@ -457,3 +458,32 @@ class TestChartGroupConstants: for _period, (title, subtitle) in PERIOD_CONFIG.items(): assert isinstance(title, str) assert isinstance(subtitle, str) + + +class TestBuildChartGroups: + """Tests for build_chart_groups.""" + + def test_png_paths_use_relative_prefix(self, configured_env): + """PNG fallback paths respect provided asset prefix.""" + out_dir = configured_env["out_dir"] + asset_dir = out_dir / "assets" / "repeater" + asset_dir.mkdir(parents=True, exist_ok=True) + (asset_dir / "bat_day_light.png").write_bytes(b"fake") + + groups = build_chart_groups( + role="repeater", + period="day", + chart_stats={}, + asset_prefix="../", + ) + + chart = next( + chart + for group in groups + for chart in group["charts"] + if chart["metric"] == "bat" + ) + + assert chart["use_svg"] is False + assert chart["src_light"] == "../assets/repeater/bat_day_light.png" + assert chart["src_dark"] == "../assets/repeater/bat_day_dark.png"