feat(web_viewer): make the Multibyte page public

Multibyte rollout shows per-node hash sizes and traffic counts taken from adverts and paths, the same mesh-visible data the public Mesh Graph serves. With web_viewer_password set it was admin-only and missing from anonymous navigation. The page, /api/multibyte-rollout and the nav link are now public whenever the multibyte monitor is on.

From #350.
This commit is contained in:
agessaman committed 2026-11-04 18:05:55 -08:00
1 parent 716531a558
commit 2be738c604
5 files changed
+21 -7

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ semantic versioning.
- `[Weather] openmeteo_fallback` (default `false`, only with `weather_provider = noaa`): `wx` answers a place outside NWS coverage from Open-Meteo, in `gwx`'s format and `wx`'s units, instead of replying with an error. Current conditions, `tomorrow`, `Nd` and `hourly` fall back; `wx <place> alerts` there replies that alerts are not available. Only NWS's no-coverage answer triggers it, not timeouts or server errors, and the point is remembered for 24 hours so later requests skip NWS. It's also in the web viewer's `wx` settings.
- `[Weather] always_show_location` (default `false`): when `true`, `wx` and `gwx` name the place in every reply a place is found for (`Lake Stevens, WA: …`), including ZIP codes and cities in the bot's own state or country, instead of only when the name adds information. The name takes message length from the forecast, and a ZIP code costs one extra reverse lookup. It's also in the web viewer's `wx` settings.
- `gwx <place> hourly`: the next hours from Open-Meteo, packed into one reply like `wx`'s NOAA hourly forecast (time, condition, chance of precipitation, temperature, wind), in the configured units and translated. `gwx` listed `hourly` in its help but read it as part of the place name. `wx` with `weather_provider = openmeteo` gets it too.
- Web viewer admin role when `web_viewer_password` is set: Dashboard, Contacts, and Mesh Graph are readable anonymously; Real-time, Radio, Config, Logs, and all mutating APIs require an admin session. Real-time's live Socket.IO streams (and `/api/channels`) can expose decrypted traffic and channel keys, so the page redirects anonymous visitors to the login page and drops out of their navigation instead of showing them an empty page.
- Web viewer admin role when `web_viewer_password` is set: Dashboard, Contacts, Mesh Graph and (when the multibyte monitor is on) Multibyte are readable anonymously; Real-time, Radio, Config, Logs, and all mutating APIs require an admin session. Real-time's live Socket.IO streams (and `/api/channels`) can expose decrypted traffic and channel keys, so the page redirects anonymous visitors to the login page and drops out of their navigation instead of showing them an empty page.
- `[Weather_Service] weather_alerts_enabled` (default `true`) turns the NOAA
weather-alert polling loop off on its own. Alert monitoring previously started
+4 -1
View File
@@ -865,7 +865,7 @@ class BotDataViewer(DashboardSnapshotMixin, LiveStreamMixin, SocketClientsMixin,
# Issue #240 public HTML surface. Realtime is admin-only: its live socket
# is, so an anonymous visit would only ever show an empty page.
_PUBLIC_PAGE_PATHS = frozenset([
'/', '/contacts', '/mesh',
'/', '/contacts', '/mesh', '/multibyte-rollout',
])
# Anonymous-safe GET APIs: mesh-visible / aggregate data only. No channel
@@ -883,6 +883,9 @@ class BotDataViewer(DashboardSnapshotMixin, LiveStreamMixin, SocketClientsMixin,
'/api/mesh/nodes',
'/api/mesh/edges',
'/api/mesh/stats',
# Multibyte rollout: per-node hash sizes and traffic counts from
# adverts and paths, all mesh-visible.
'/api/multibyte-rollout',
])
# Read-only POST helpers used by public Contacts / Mesh info panels.
+1 -1
View File
@@ -826,7 +826,7 @@
<i class="fas fa-project-diagram"></i> Mesh Graph
</a>
</li>
{% if multibyte_monitor_enabled and (not auth_enabled or is_admin) %}
{% if multibyte_monitor_enabled %}
<li class="nav-item">
<a class="nav-link {{ 'active' if request.path == '/multibyte-rollout' }}" href="/multibyte-rollout">
<i class="fas fa-code-branch"></i> Multibyte
@@ -1039,10 +1039,10 @@
},
"/api/multibyte-rollout": {
"GET locked anon": {
"content_type": "application/json",
"content_type": "text/html",
"csp_script_src": "script-src 'self' 'unsafe-inline'",
"location": "",
"status": 401,
"status": 404,
"x_content_type_options": "nosniff",
"x_frame_options": "SAMEORIGIN"
},
@@ -1750,8 +1750,8 @@
"GET locked anon": {
"content_type": "text/html",
"csp_script_src": "script-src 'self' 'nonce-X'",
"location": "/login?next=/multibyte-rollout",
"status": 302,
"location": "",
"status": 404,
"x_content_type_options": "nosniff",
"x_frame_options": "SAMEORIGIN"
},
+11
View File
@@ -1282,6 +1282,17 @@ class TestRoleBasedAccess:
assert auth_client.get("/realtime").status_code == 200
assert 'href="/realtime"' in auth_client.get("/").get_data(as_text=True)
def test_multibyte_page_and_data_are_public(self, auth_viewer, auth_client):
"""Multibyte rollout is mesh-visible data, so anonymous visitors get the page, its data and the nav link."""
auth_viewer.multibyte_monitor_enabled = True
assert auth_client.get("/multibyte-rollout").status_code == 200
resp = auth_client.get("/api/multibyte-rollout")
assert resp.status_code == 200
blob = str(resp.get_json()).lower()
for needle in ("smtp_password", "channel_key", "key_hex", "private_key", "web_viewer_password"):
assert needle not in blob
assert 'href="/multibyte-rollout"' in auth_client.get("/").get_data(as_text=True)
def test_admin_pages_require_auth(self, auth_client):
admin_pages = ["/logs", "/config", "/plugins", "/radio", "/greeter", "/feeds", "/schedule"]
for page in admin_pages: