diff --git a/meshinfo/config.py b/meshinfo/config.py index 1f7b0df..081658c 100644 --- a/meshinfo/config.py +++ b/meshinfo/config.py @@ -152,7 +152,7 @@ def configure( filters = settings.setdefault("jinja2.filters", {}) filters.setdefault("duration", "meshinfo.filters.duration") filters.setdefault("in_tz", "meshinfo.filters.in_tz") - filters.setdefault("local_tz", "meshinfo.filters.local_tz") + filters.setdefault("client_tz", "meshinfo.filters.client_tz") if app_config.env == Environment.DEV: settings["pyramid.reload_all"] = True @@ -185,23 +185,20 @@ def configure( if app_config.env == Environment.DEV: config.include("pyramid_debugtoolbar") - server_timezone = pendulum.tz.local_timezone() - def client_timezone(request): - if "local_tz" in request.cookies: - try: - client_tz = pendulum.timezone(request.cookies["local_tz"]) - except Exception as exc: - # TODO: identify client? - logger.warning( - "Invalid timezone specified: {} ({!r})", - request.cookies["local_tz"], - exc, - ) - client_tz = server_timezone - return client_tz.name - else: - return server_timezone.name + if "client_tz" not in request.cookies: + return pendulum.timezone("UTC") + try: + client_tz = pendulum.timezone(request.cookies["client_tz"]) + except Exception as exc: + # TODO: identify client? + logger.warning( + "Invalid timezone specified: {} ({!r})", + request.cookies["client_tz"], + exc, + ) + client_tz = pendulum.timezone("UTC") + return client_tz config.add_request_method(lambda r: client_timezone(r), "timezone", reify=True) diff --git a/meshinfo/filters.py b/meshinfo/filters.py index 7cd5687..efc57f2 100644 --- a/meshinfo/filters.py +++ b/meshinfo/filters.py @@ -18,6 +18,6 @@ def in_tz(dt, tz="utc"): @jinja2.pass_context -def local_tz(ctx, dt): +def client_tz(ctx, dt): request = ctx.get("request") or get_current_request() return dt.in_tz(request.timezone).format("YYYY-MM-DD HH:mm:ss zz") diff --git a/meshinfo/historical.py b/meshinfo/historical.py index 3cf6d18..b100185 100644 --- a/meshinfo/historical.py +++ b/meshinfo/historical.py @@ -3,6 +3,7 @@ from __future__ import annotations import enum +import os from itertools import cycle from pathlib import Path from typing import Any, Iterable, List, Optional @@ -132,6 +133,7 @@ def graph_network_stats( self, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph network poller statistics.""" rrd_file = self._network_filename() @@ -150,9 +152,9 @@ def graph_network_stats( legend=ds.replace("_count", "s"), ) - return graph.render() + return graph.render(timezone=timezone) - def graph_poller_stats(self, *, params: GraphParams) -> bytes: + def graph_poller_stats(self, *, params: GraphParams, timezone: str) -> bytes: """Graph network info.""" rrd_file = self._network_filename() colors = cycle(COLORS) @@ -170,13 +172,14 @@ def graph_poller_stats(self, *, params: GraphParams) -> bytes: style="LINE1", legend=ds.replace("_time", ""), ) - return graph.render() + return graph.render(timezone=timezone) def graph_node_uptime( self, node: Node, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph node uptime.""" rrd_file = self._node_filename(node) @@ -193,13 +196,14 @@ def graph_node_uptime( style="AREA", legend="uptime", ) - return graph.render() + return graph.render(timezone=timezone) def graph_node_load( self, node: Node, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph node uptime.""" rrd_file = self._node_filename(node) @@ -216,13 +220,14 @@ def graph_node_load( style="LINE1", legend="load", ) - return graph.render() + return graph.render(timezone=timezone) def graph_node_links( self, node: Node, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph node links.""" rrd_file = self._node_filename(node) @@ -256,13 +261,14 @@ def graph_node_links( style="LINE1", ) - return graph.render() + return graph.render(timezone=timezone) def graph_link_cost( self, link: Link, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph link routing cost.""" @@ -278,13 +284,14 @@ def graph_link_cost( style="LINE1", legend="route cost", ) - return graph.render() + return graph.render(timezone=timezone) def graph_link_snr( self, link: Link, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph node uptime.""" # TODO: add a black line at 0 so it stands out @@ -316,13 +323,14 @@ def graph_link_snr( style="LINE1", legend="noise", ) - return graph.render() + return graph.render(timezone=timezone) def graph_link_quality( self, link: Link, *, params: GraphParams, + timezone: str, ) -> bytes: """Graph link quality and neighbor quality.""" @@ -347,7 +355,7 @@ def graph_link_quality( style="LINE1", legend="neighbor", ) - return graph.render() + return graph.render(timezone=timezone) def update_link_stats(self, link: Link) -> bool: # switch to async after testing! @@ -590,8 +598,8 @@ def add_summarized_ds( ) self.elements[-1] += r"\l" - def render(self) -> bytes: - """Draw the graph via RRDtool.""" + def render(self, *, timezone: str) -> bytes: + """Draw the graph via RRDtool in a particular timezone.""" self.options.extend(("--start", self.start)) if self.end: @@ -616,5 +624,11 @@ def render(self) -> bytes: ) logger.trace("Rendering graph: {}", graphv_args) + current_timezone = os.getenv("TZ") + os.environ["TZ"] = timezone graph_info = rrdtool.graphv(*graphv_args) + if current_timezone is None: + del os.environ["TZ"] + else: + os.environ["TZ"] = current_timezone return graph_info["image"] diff --git a/meshinfo/templates/home.jinja2 b/meshinfo/templates/home.jinja2 index 0a53629..5279450 100644 --- a/meshinfo/templates/home.jinja2 +++ b/meshinfo/templates/home.jinja2 @@ -18,7 +18,7 @@
Last Ran
-
{{ last_run.started_at|local_tz }}
+
{{ last_run.started_at|client_tz }}
Polling Time
diff --git a/meshinfo/templates/network-errors.jinja2 b/meshinfo/templates/network-errors.jinja2 index f8d816f..d68a899 100644 --- a/meshinfo/templates/network-errors.jinja2 +++ b/meshinfo/templates/network-errors.jinja2 @@ -4,17 +4,17 @@ {% block content %}
-

Node Polling Errors at {{ stats.started_at|local_tz }}

+

Node Polling Errors at {{ stats.started_at|client_tz }}

Started At
-
{{ stats.started_at|local_tz }}
+
{{ stats.started_at|client_tz }}
Finished At
-
{{ stats.finished_at|local_tz }}
+
{{ stats.finished_at|client_tz }}
diff --git a/meshinfo/templates/node-details.jinja2 b/meshinfo/templates/node-details.jinja2 index 41b6cd3..6289a42 100644 --- a/meshinfo/templates/node-details.jinja2 +++ b/meshinfo/templates/node-details.jinja2 @@ -38,7 +38,7 @@
Last Seen
-
{{ node.last_seen|local_tz }}
+
{{ node.last_seen|client_tz }}
AREDN Page @@ -153,7 +153,7 @@
Last Seen
-
{{ link.last_seen|local_tz }}
+
{{ link.last_seen|client_tz }}
diff --git a/meshinfo/templates/nodes.jinja2 b/meshinfo/templates/nodes.jinja2 index 3c69016..ce32962 100644 --- a/meshinfo/templates/nodes.jinja2 +++ b/meshinfo/templates/nodes.jinja2 @@ -87,7 +87,7 @@ '{{ node.active_tunnel_count }}', '{{ node.firmware_version }}', '{{ node.up_time }}', - '{{ node.last_seen|local_tz }}' + '{{ node.last_seen|client_tz }}' ]{% if not loop.last %},{% endif %} {% endfor %} ], diff --git a/meshinfo/views/link.py b/meshinfo/views/link.py index 5baae50..040f7aa 100644 --- a/meshinfo/views/link.py +++ b/meshinfo/views/link.py @@ -105,6 +105,7 @@ def __init__(self, request: Request): self.graph_params = schema.graph_params(request.GET) self.stats: HistoricalStats = request.find_service(HistoricalStats) + self.timezone: str = request.timezone.name @view_config(match_param="name=cost") def cost_graph(self): @@ -114,8 +115,11 @@ def cost_graph(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_link_cost( + self.link, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_link_cost(self.link, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) @@ -128,8 +132,11 @@ def snr_graph(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_link_snr( + self.link, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_link_snr(self.link, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) @@ -142,8 +149,11 @@ def quality_graph(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_link_quality( + self.link, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_link_quality(self.link, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) diff --git a/meshinfo/views/network.py b/meshinfo/views/network.py index 6e0b15f..ac46e6d 100644 --- a/meshinfo/views/network.py +++ b/meshinfo/views/network.py @@ -27,6 +27,7 @@ class NetworkGraphs: def __init__(self, request: Request): self.graph_params = schema.graph_params(request.GET) self.stats: HistoricalStats = request.find_service(HistoricalStats) + self.timezone: str = request.timezone.name @view_config(match_param="name=info") def info(self): @@ -35,9 +36,11 @@ def info(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) - + graph_data = self.stats.graph_network_stats( + params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_network_stats(params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) @@ -49,9 +52,11 @@ def poller(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) - + graph_data = self.stats.graph_poller_stats( + params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_poller_stats(params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) diff --git a/meshinfo/views/node.py b/meshinfo/views/node.py index 2ae26f3..e91de18 100644 --- a/meshinfo/views/node.py +++ b/meshinfo/views/node.py @@ -126,6 +126,7 @@ def __init__(self, request: Request): self.name_in_title = asbool(request.GET.get("name_in_title", False)) self.stats: HistoricalStats = request.find_service(HistoricalStats) + self.timezone: str = request.timezone.name @view_config(match_param="name=links") def links(self): @@ -135,8 +136,11 @@ def links(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_node_links( + self.node, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_node_links(self.node, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) @@ -149,8 +153,11 @@ def load(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_node_load( + self.node, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_node_load(self.node, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", ) @@ -163,8 +170,11 @@ def uptime(self): self.graph_params.title, ) self.graph_params.title = " - ".join(part for part in title_parts if part) + graph_data = self.stats.graph_node_uptime( + self.node, params=self.graph_params, timezone=self.timezone + ) return Response( - self.stats.graph_node_uptime(self.node, params=self.graph_params), + graph_data, status="200 OK", content_type="image/png", )