diff --git a/meshview/templates/reach.html b/meshview/templates/reach.html
new file mode 100644
index 0000000..09b4030
--- /dev/null
+++ b/meshview/templates/reach.html
@@ -0,0 +1,308 @@
+{% extends "base.html" %}
+
+{% block css %}
+.reach-page {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 16px;
+}
+
+.reach-header {
+ margin-bottom: 16px;
+}
+
+.reach-header h1 {
+ font-size: 1.6rem;
+ margin-bottom: 6px;
+}
+
+.reach-panel {
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 8px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.reach-node-id {
+ font-family: monospace;
+ font-size: 1.1rem;
+}
+
+.reach-controls {
+ display: flex;
+ gap: 10px;
+ margin-top: 14px;
+}
+
+.reach-search {
+ min-width: 0;
+ flex: 1;
+}
+
+@media (max-width: 600px) {
+ .reach-controls {
+ flex-direction: column;
+ }
+}
+
+.reach-summary {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.reach-metric {
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 8px;
+ padding: 12px;
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.reach-metric-value {
+ font-size: 1.5rem;
+ line-height: 1.2;
+}
+
+.reach-table-wrap {
+ overflow-x: auto;
+ margin-top: 18px;
+}
+
+.reach-table {
+ width: 100%;
+ min-width: 760px;
+}
+
+.reach-table th,
+.reach-table td {
+ vertical-align: top;
+}
+
+.reach-muted {
+ color: rgba(255, 255, 255, 0.62);
+}
+{% endblock %}
+
+{% block body %}
+
+
+
+
+ Selected Node ID
+ 3530776522
+
+
+
+
+
+
+
Unique Gateways
+
...
+
+
+
+
+
+
+
+ | Gateway |
+ Heard Packets |
+ Total Packets |
+ Reach |
+
+
+
+
+ | Loading packets... |
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/meshview/web.py b/meshview/web.py
index e8f7f25..3772ab8 100644
--- a/meshview/web.py
+++ b/meshview/web.py
@@ -259,6 +259,12 @@ async def map(request):
return web.Response(text=template.render(), content_type="text/html")
+@routes.get("/reach")
+async def reach(request):
+ template = env.get_template("reach.html")
+ return web.Response(text=template.render(), content_type="text/html")
+
+
@routes.get("/nodelist")
async def nodelist(request):
template = env.get_template("nodelist.html")