mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
* Add instance selector for federation regions * Avoid HTML insertion when seeding instance selector
239 lines
13 KiB
Plaintext
239 lines
13 KiB
Plaintext
<!doctype html>
|
||
|
||
<!--
|
||
Copyright (C) 2025 l5yth
|
||
|
||
Licensed under the Apache License, Version 2.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
You may obtain a copy of the License at
|
||
|
||
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
||
Unless required by applicable law or agreed to in writing, software
|
||
distributed under the License is distributed on an "AS IS" BASIS,
|
||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
-->
|
||
|
||
<html lang="en" data-theme="<%= initial_theme %>">
|
||
<head>
|
||
<meta name="color-scheme" content="dark light">
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<% meta_title_html = Rack::Utils.escape_html(meta_title) %>
|
||
<% meta_name_html = Rack::Utils.escape_html(meta_name) %>
|
||
<% meta_description_html = Rack::Utils.escape_html(meta_description) %>
|
||
<% request_path = request.path.to_s.empty? ? "/" : request.path %>
|
||
<% canonical_url = "#{request.base_url}#{request_path}" %>
|
||
<% canonical_html = Rack::Utils.escape_html(canonical_url) %>
|
||
<% logo_url = "#{request.base_url}/potatomesh-logo.svg" %>
|
||
<% logo_url_html = Rack::Utils.escape_html(logo_url) %>
|
||
<% logo_alt_html = Rack::Utils.escape_html("#{meta_name} logo") %>
|
||
<title><%= meta_title_html %></title>
|
||
<meta name="application-name" content="<%= meta_name_html %>" />
|
||
<meta name="apple-mobile-web-app-title" content="<%= meta_name_html %>" />
|
||
<meta name="description" content="<%= meta_description_html %>" />
|
||
<link rel="canonical" href="<%= canonical_html %>" />
|
||
<meta property="og:title" content="<%= meta_title_html %>" />
|
||
<meta property="og:site_name" content="<%= meta_name_html %>" />
|
||
<meta property="og:description" content="<%= meta_description_html %>" />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:url" content="<%= canonical_html %>" />
|
||
<meta property="og:image" content="<%= logo_url_html %>" />
|
||
<meta property="og:image:alt" content="<%= logo_alt_html %>" />
|
||
<meta name="twitter:card" content="summary" />
|
||
<meta name="twitter:title" content="<%= meta_title_html %>" />
|
||
<meta name="twitter:description" content="<%= meta_description_html %>" />
|
||
<meta name="twitter:image" content="<%= logo_url_html %>" />
|
||
<meta name="twitter:image:alt" content="<%= logo_alt_html %>" />
|
||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||
<link rel="icon" type="image/svg+xml" href="/potatomesh-logo.svg" />
|
||
<link rel="stylesheet" href="/assets/styles/base.css" />
|
||
<script src="/assets/js/theme.js" defer></script>
|
||
<script src="/assets/js/background.js" defer></script>
|
||
|
||
<!-- Leaflet CSS/JS (CDN) --> <!-- Leaflet CSS/JS (CDN) -->
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||
crossorigin=""
|
||
/>
|
||
<script
|
||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||
crossorigin=""
|
||
></script>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</head>
|
||
<% body_classes = [] %>
|
||
<% body_classes << "dark" if initial_theme == "dark" %>
|
||
<body class="<%= body_classes.join(" ") %>" data-app-config="<%= Rack::Utils.escape_html(app_config_json) %>" data-theme="<%= initial_theme %>">
|
||
<div class="site-header">
|
||
<h1 class="site-title">
|
||
<img src="/potatomesh-logo.svg" alt="" aria-hidden="true" />
|
||
<span class="site-title-text"><%= site_name %></span>
|
||
</h1>
|
||
<% if !private_mode && federation_enabled %>
|
||
<div class="instance-selector">
|
||
<label class="visually-hidden" for="instanceSelect">Select a region</label>
|
||
<select id="instanceSelect" class="instance-select" aria-label="Select instance region">
|
||
<option value=""><%= Rack::Utils.escape_html("Select region ...") %></option>
|
||
</select>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
<div class="row meta">
|
||
<div class="meta-info">
|
||
<div class="refresh-row">
|
||
<p id="refreshInfo" class="refresh-info" aria-live="polite"><%= channel %> (<%= frequency %>) — active nodes: …</p>
|
||
<div class="refresh-actions">
|
||
<label class="auto-refresh-toggle"><input type="checkbox" id="autoRefresh" checked /> Auto-refresh every <%= refresh_interval_seconds %> seconds</label>
|
||
<button id="refreshBtn" type="button">Refresh now</button>
|
||
<span id="status" class="pill">loading…</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="controls">
|
||
<label><input type="checkbox" id="fitBounds" checked /> Auto-fit map</label>
|
||
<div class="filter-input">
|
||
<input type="text" id="filterInput" placeholder="Filter nodes" />
|
||
<button type="button" id="filterClear" class="filter-clear" aria-label="Clear filter" hidden>×</button>
|
||
</div>
|
||
<button id="themeToggle" class="icon-button" type="button" aria-label="Toggle dark mode"><span aria-hidden="true">🌙</span></button>
|
||
<button id="infoBtn" class="icon-button" type="button" aria-haspopup="dialog" aria-controls="infoOverlay" aria-label="Show site information"><span aria-hidden="true">ℹ️</span></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="infoOverlay" class="info-overlay" role="dialog" aria-modal="true" aria-labelledby="infoTitle" hidden>
|
||
<div class="info-dialog" tabindex="-1">
|
||
<button type="button" class="info-close" id="infoClose" aria-label="Close site information">×</button>
|
||
<h2 id="infoTitle" class="info-title">About <%= site_name %></h2>
|
||
<dl class="info-details">
|
||
<dt>Channel</dt>
|
||
<dd><%= channel %></dd>
|
||
<dt>Frequency</dt>
|
||
<dd><%= frequency %></dd>
|
||
<dt>Map center</dt>
|
||
<dd><%= format("%.5f, %.5f", map_center_lat, map_center_lon) %></dd>
|
||
<dt>Visible range</dt>
|
||
<dd>Nodes within roughly <%= max_distance_km %> km of the center are shown.</dd>
|
||
<% if contact_link && !contact_link.empty? %>
|
||
<dt>Chat</dt>
|
||
<% if contact_link_url %>
|
||
<dd><a href="<%= contact_link_url %>" target="_blank" rel="noreferrer noopener"><%= contact_link %></a></dd>
|
||
<% else %>
|
||
<dd><%= contact_link %></dd>
|
||
<% end %>
|
||
<% end %>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="map-row">
|
||
<% unless private_mode %>
|
||
<div id="chat" aria-label="Chat log"></div>
|
||
<% end %>
|
||
<div class="map-panel" id="mapPanel">
|
||
<div id="map" role="region" aria-label="Nodes map"></div>
|
||
<div class="map-toolbar" role="group" aria-label="Map view controls">
|
||
<button id="mapFullscreenToggle" type="button" aria-pressed="false" aria-label="Enter full screen map view">
|
||
<svg class="icon-fullscreen-enter" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||
<path
|
||
d="M4 8V4h4m12 4V4h-4M4 16v4h4m12-4v4h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
<svg class="icon-fullscreen-exit" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||
<path
|
||
d="M9 9L5 5m0 4V5m4 0H5m10 4 4-4m0 4V5m-4 0h4M9 15l-4 4m0-4v4m4 0H5m10-4 4 4m0-4v4m-4 0h4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<table id="nodes">
|
||
<thead>
|
||
<tr>
|
||
<th class="nodes-col nodes-col--node-id"><button type="button" class="sort-button" data-sort-key="node_id" data-sort-label="Node ID">Node ID <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--short-name"><button type="button" class="sort-button" data-sort-key="short_name" data-sort-label="Short Name">Short <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--long-name"><button type="button" class="sort-button" data-sort-key="long_name" data-sort-label="Long Name">Long Name <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--last-seen"><button type="button" class="sort-button" data-sort-key="last_heard" data-sort-label="Last Seen">Last Seen <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--role"><button type="button" class="sort-button" data-sort-key="role" data-sort-label="Role">Role <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--hw-model"><button type="button" class="sort-button" data-sort-key="hw_model" data-sort-label="Hardware Model">HW Model <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--battery"><button type="button" class="sort-button" data-sort-key="battery_level" data-sort-label="Battery Level">Battery <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--voltage"><button type="button" class="sort-button" data-sort-key="voltage" data-sort-label="Voltage">Voltage <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--uptime"><button type="button" class="sort-button" data-sort-key="uptime_seconds" data-sort-label="Uptime">Uptime <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--channel-util"><button type="button" class="sort-button" data-sort-key="channel_utilization" data-sort-label="Channel Utilization">Channel Util <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--air-util-tx"><button type="button" class="sort-button" data-sort-key="air_util_tx" data-sort-label="Air Utilization (Tx)">Air Util Tx <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--temperature"><button type="button" class="sort-button" data-sort-key="temperature" data-sort-label="Temperature">Temperature <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--humidity"><button type="button" class="sort-button" data-sort-key="relative_humidity" data-sort-label="Humidity">Humidity <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--pressure"><button type="button" class="sort-button" data-sort-key="barometric_pressure" data-sort-label="Barometric Pressure">Pressure <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--latitude"><button type="button" class="sort-button" data-sort-key="latitude" data-sort-label="Latitude">Latitude <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--longitude"><button type="button" class="sort-button" data-sort-key="longitude" data-sort-label="Longitude">Longitude <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--altitude"><button type="button" class="sort-button" data-sort-key="altitude" data-sort-label="Altitude">Altitude <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
<th class="nodes-col nodes-col--last-position"><button type="button" class="sort-button" data-sort-key="position_time" data-sort-label="Last Position">Last Position <span class="sort-indicator" aria-hidden="true"></span></button></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
|
||
<template id="shortInfoOverlayTemplate">
|
||
<div class="short-info-overlay" role="dialog" aria-modal="false">
|
||
<button type="button" class="short-info-close" aria-label="Close node details">×</button>
|
||
<div class="short-info-content"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<footer class="app-footer">
|
||
<div class="footer-content">
|
||
<span class="footer-brand">PotatoMesh</span>
|
||
<% if version && !version.empty? %>
|
||
<span class="mono"><%= version %></span>
|
||
<% end %>
|
||
<span class="footer-separator" aria-hidden="true">—</span>
|
||
<span class="footer-links">
|
||
GitHub:
|
||
<a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
|
||
<% if contact_link && !contact_link.empty? %>
|
||
<span class="footer-separator" aria-hidden="true">—</span>
|
||
<span class="footer-contact">
|
||
<%= site_name %> chat:
|
||
<% if contact_link_url %>
|
||
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
|
||
<% else %>
|
||
<%= contact_link %>
|
||
<% end %>
|
||
</span>
|
||
<% end %>
|
||
</span>
|
||
</div>
|
||
</footer>
|
||
|
||
|
||
|
||
<script>
|
||
const CHAT_ENABLED = <%= private_mode ? "false" : "true" %>;
|
||
</script>
|
||
<script type="module" src="/assets/js/app/index.js"></script>
|
||
</body>
|
||
</html>
|