mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-08 09:52:44 +02:00
worked on making map and base all API driven
This commit is contained in:
@@ -27,11 +27,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<!-- Dynamic Header -->
|
||||
<div style="text-align:center" id="site-header"></div>
|
||||
<div style="text-align:center" id="site-message"></div>
|
||||
|
||||
<!-- Dynamic Menu -->
|
||||
<div style="text-align:center" id="site-menu"></div>
|
||||
|
||||
<!-- Search Form -->
|
||||
@@ -57,10 +54,7 @@
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
|
||||
<select name="portnum" class="col-2 m-2" id="portnum_select">
|
||||
<!-- Options populated dynamically -->
|
||||
</select>
|
||||
|
||||
<select name="portnum" class="col-2 m-2" id="portnum_select"></select>
|
||||
<input type="submit" value="Go to Node" class="col-2 m-2" data-translate-lang="go to node" />
|
||||
</div>
|
||||
</form>
|
||||
@@ -69,105 +63,68 @@
|
||||
|
||||
<br>
|
||||
<div style="text-align:center" id="footer" data-translate-lang="footer"></div>
|
||||
<div style="text-align:center"><div><small id="site-version">ver. unknown</small></div></div>
|
||||
<div style="text-align:center"><small id="site-version">ver. unknown</small></div>
|
||||
<br>
|
||||
|
||||
<!-- Language Loader -->
|
||||
<!-- Shared Site Config -->
|
||||
<script>
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
const langCode = "en";
|
||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=base`);
|
||||
const t = await res.json();
|
||||
|
||||
document.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
const key = el.getAttribute("data-translate-lang");
|
||||
if (t[key]) {
|
||||
if (el.placeholder !== undefined && el.tagName === "INPUT" && el.type === "text") {
|
||||
el.placeholder = t[key];
|
||||
} else if (el.tagName === "INPUT" && el.type === "submit") {
|
||||
el.value = t[key];
|
||||
} else {
|
||||
el.innerHTML = t[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Portnum options
|
||||
const select = document.getElementById("portnum_select");
|
||||
if (select && t.portnum_options) {
|
||||
select.innerHTML = ""; // Clear
|
||||
const allOption = document.createElement("option");
|
||||
allOption.value = "";
|
||||
allOption.textContent = t["all"] || "All";
|
||||
if ("{{portnum}}" === "") allOption.selected = true;
|
||||
select.appendChild(allOption);
|
||||
|
||||
for (const [value, label] of Object.entries(t.portnum_options)) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = value;
|
||||
opt.textContent = label;
|
||||
if ("{{portnum}}" === String(value)) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load language:", err);
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", loadTranslations);
|
||||
</script>
|
||||
|
||||
<!-- Dynamic Site Config Loader -->
|
||||
<script>
|
||||
async function loadSiteConfig() {
|
||||
// Global shared config promise (only fetch once)
|
||||
if (!window._siteConfigPromise) {
|
||||
window._siteConfigPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch("/api/config");
|
||||
const cfg = await res.json();
|
||||
const site = cfg.site || {};
|
||||
|
||||
// Title
|
||||
document.title = "Meshview - " + (site.title || "");
|
||||
|
||||
// Header
|
||||
const header = document.getElementById("site-header");
|
||||
if (header) {
|
||||
header.innerHTML = `<strong>${site.title || ""} ${site.domain ? "(" + site.domain + ")" : ""}</strong>`;
|
||||
}
|
||||
|
||||
// Message
|
||||
const msg = document.getElementById("site-message");
|
||||
if (msg) {
|
||||
msg.textContent = site.message || "";
|
||||
}
|
||||
|
||||
// Menu
|
||||
const menu = document.getElementById("site-menu");
|
||||
if (menu) {
|
||||
let html = "";
|
||||
if (site.nodes === "true") html += `<a href="/nodelist">Nodes</a>`;
|
||||
if (site.conversations === "true") html += ` - <a href="/chat">Conversations</a>`;
|
||||
if (site.everything === "true") html += ` - <a href="/firehose">See Everything</a>`;
|
||||
if (site.graphs === "true") html += ` - <a href="/nodegraph">Mesh Graphs</a>`;
|
||||
if (site.net === "true") html += ` - <a href="/net">Weekly Net</a>`;
|
||||
if (site.map === "true") html += ` - <a href="/map">Live Map</a>`;
|
||||
if (site.stats === "true") html += ` - <a href="/stats">Stats</a>`;
|
||||
if (site.top === "true") html += ` - <a href="/top">Top Traffic Nodes</a>`;
|
||||
menu.innerHTML = html;
|
||||
}
|
||||
|
||||
// Version
|
||||
const verEl = document.getElementById("site-version");
|
||||
if (verEl) {
|
||||
verEl.textContent = "ver. " + (site.version || "unknown");
|
||||
}
|
||||
|
||||
const config = await res.json();
|
||||
window._siteConfig = config;
|
||||
console.log("Loaded config from /api/config:", config);
|
||||
return config;
|
||||
} catch (err) {
|
||||
console.error("Failed to load site config:", err);
|
||||
console.error("Failed to load /api/config:", err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadSiteConfig);
|
||||
</script>
|
||||
async function loadSiteConfig() {
|
||||
try {
|
||||
const cfg = await window._siteConfigPromise;
|
||||
const site = cfg.site || {};
|
||||
|
||||
// Title
|
||||
document.title = "Meshview - " + (site.title || "");
|
||||
|
||||
// Header
|
||||
const header = document.getElementById("site-header");
|
||||
if (header)
|
||||
header.innerHTML = `<strong>${site.title || ""} ${site.domain ? "(" + site.domain + ")" : ""}</strong>`;
|
||||
|
||||
// Message
|
||||
const msg = document.getElementById("site-message");
|
||||
if (msg) msg.textContent = site.message || "";
|
||||
|
||||
// Menu
|
||||
const menu = document.getElementById("site-menu");
|
||||
if (menu) {
|
||||
let html = "";
|
||||
if (site.nodes === "true") html += `<a href="/nodelist">Nodes</a>`;
|
||||
if (site.conversations === "true") html += ` - <a href="/chat">Conversations</a>`;
|
||||
if (site.everything === "true") html += ` - <a href="/firehose">See Everything</a>`;
|
||||
if (site.graphs === "true") html += ` - <a href="/nodegraph">Mesh Graphs</a>`;
|
||||
if (site.net === "true") html += ` - <a href="/net">Weekly Net</a>`;
|
||||
if (site.map === "true") html += ` - <a href="/map">Live Map</a>`;
|
||||
if (site.stats === "true") html += ` - <a href="/stats">Stats</a>`;
|
||||
if (site.top === "true") html += ` - <a href="/top">Top Traffic Nodes</a>`;
|
||||
menu.innerHTML = html;
|
||||
}
|
||||
|
||||
// Version
|
||||
const verEl = document.getElementById("site-version");
|
||||
if (verEl) verEl.textContent = "ver. " + (site.version || "unknown");
|
||||
} catch (err) {
|
||||
console.error("Failed to load site config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadSiteConfig);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block css %}
|
||||
.timestamp {
|
||||
min-width: 10em;
|
||||
}
|
||||
.timestamp { min-width: 10em; }
|
||||
.chat-packet:nth-of-type(odd) { background-color: #3a3a3a; }
|
||||
.chat-packet { border-bottom: 1px solid #555; padding: 8px; border-radius: 8px; }
|
||||
.chat-packet:nth-of-type(even) { background-color: #333333; }
|
||||
|
||||
@keyframes flash {
|
||||
0% { background-color: #ffe066; }
|
||||
100% { background-color: inherit; }
|
||||
}
|
||||
@keyframes flash { 0% { background-color: #ffe066; } 100% { background-color: inherit; } }
|
||||
.chat-packet.flash { animation: flash 3.5s ease-out; }
|
||||
|
||||
/* Nested reply style */
|
||||
.replying-to { font-size: 0.85em; color: #aaa; margin-top: 4px; padding-left: 20px; }
|
||||
.replying-to .reply-preview { color: #aaa; }
|
||||
{% endblock %}
|
||||
@@ -36,11 +30,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : text;
|
||||
div.textContent = text ?? "";
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 🔑 helper to apply translations
|
||||
function applyTranslations(translations, root=document) {
|
||||
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
const key = el.dataset.translateLang;
|
||||
@@ -58,7 +51,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
packetMap.set(packet.id, packet);
|
||||
|
||||
const date = new Date(packet.import_time);
|
||||
const formattedTime = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true });
|
||||
const formattedTime = date.toLocaleTimeString([], { hour:"numeric", minute:"2-digit", second:"2-digit", hour12:true });
|
||||
const formattedDate = `${(date.getMonth()+1).toString().padStart(2,"0")}/${date.getDate().toString().padStart(2,"0")}/${date.getFullYear()}`;
|
||||
const formattedTimestamp = `${formattedTime} - ${formattedDate}`;
|
||||
|
||||
@@ -66,22 +59,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (packet.reply_id) {
|
||||
const parent = packetMap.get(packet.reply_id);
|
||||
if (parent) {
|
||||
replyHtml = `
|
||||
<div class="replying-to">
|
||||
<div class="reply-preview">
|
||||
<i data-translate-lang="replying_to"></i>
|
||||
<strong>${escapeHtml((parent.long_name || "").trim() || `Node ${parent.from_node_id}`)}</strong>:
|
||||
${escapeHtml(parent.payload || "")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
replyHtml = `
|
||||
<div class="replying-to">
|
||||
replyHtml = `<div class="replying-to">
|
||||
<div class="reply-preview">
|
||||
<i data-translate-lang="replying_to"></i>
|
||||
<a href="/packet/${packet.reply_id}">${packet.reply_id}</a>
|
||||
<strong>${escapeHtml((parent.long_name || "").trim() || `Node ${parent.from_node_id}`)}</strong>:
|
||||
${escapeHtml(parent.payload || "")}
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
} else {
|
||||
replyHtml = `<div class="replying-to">
|
||||
<i data-translate-lang="replying_to"></i>
|
||||
<a href="/packet/${packet.reply_id}">${packet.reply_id}</a>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +80,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
div.innerHTML = `
|
||||
<span class="col-2 timestamp" title="${packet.import_time}">${formattedTimestamp}</span>
|
||||
<span class="col-2 channel">
|
||||
<a href="/packet/${packet.id}" title="" data-translate-lang-title="view_packet_details">✉️</a>
|
||||
<a href="/packet/${packet.id}" data-translate-lang-title="view_packet_details">✉️</a>
|
||||
${escapeHtml(packet.channel || "")}
|
||||
</span>
|
||||
<span class="col-3 nodename">
|
||||
@@ -102,17 +91,15 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
<span class="col-5 message">${escapeHtml(packet.payload)}${replyHtml}</span>
|
||||
`;
|
||||
chatContainer.prepend(div);
|
||||
|
||||
// Apply translations to the newly added packet
|
||||
applyTranslations(chatTranslations, div);
|
||||
|
||||
if (highlight) setTimeout(() => div.classList.remove("flash"), 2500);
|
||||
}
|
||||
|
||||
function renderPacketsEnsureDescending(packets, highlight = false) {
|
||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||
const sortedDesc = packets.slice().sort((a,b) => new Date(b.import_time)-new Date(a.import_time));
|
||||
for (let i = sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i], highlight);
|
||||
function renderPacketsEnsureDescending(packets, highlight=false) {
|
||||
if (!Array.isArray(packets) || packets.length===0) return;
|
||||
const sortedDesc = packets.slice().sort((a,b)=>new Date(b.import_time)-new Date(a.import_time));
|
||||
for (let i=sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i], highlight);
|
||||
}
|
||||
|
||||
async function fetchInitial() {
|
||||
@@ -121,14 +108,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const data = await resp.json();
|
||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets);
|
||||
lastTime = data?.latest_import_time || lastTime;
|
||||
} catch(err) { console.error("Initial fetch error:", err); }
|
||||
} catch(err){ console.error("Initial fetch error:", err); }
|
||||
}
|
||||
|
||||
async function fetchUpdates() {
|
||||
try {
|
||||
const url = new URL("/api/chat", window.location.origin);
|
||||
url.searchParams.set("limit","100");
|
||||
if(lastTime) url.searchParams.set("since", lastTime);
|
||||
if (lastTime) url.searchParams.set("since", lastTime);
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets, true);
|
||||
@@ -138,7 +125,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
const langCode = "{{ site_config.get('site', {}).get('language','en') }}";
|
||||
const cfg = await window._siteConfigPromise;
|
||||
const langCode = cfg?.site?.language || "en";
|
||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`);
|
||||
chatTranslations = await res.json();
|
||||
applyTranslations(chatTranslations, document);
|
||||
|
||||
@@ -178,19 +178,17 @@ function portLabel(portnum, payload) {
|
||||
<span class="text-secondary">(${portnum})</span>`;
|
||||
}
|
||||
|
||||
// --- Load config ---
|
||||
async function loadConfig() {
|
||||
// --- Fetch firehose interval from shared site config ---
|
||||
async function configureFirehose() {
|
||||
try {
|
||||
const res = await fetch("/api/config");
|
||||
if (!res.ok) return;
|
||||
const cfg = await res.json();
|
||||
const cfg = await window._siteConfigPromise;
|
||||
const intervalSec = cfg?.site?.firehose_interval;
|
||||
if (intervalSec && !isNaN(intervalSec)) {
|
||||
updateInterval = parseInt(intervalSec) * 1000;
|
||||
}
|
||||
console.log("Firehose update interval:", updateInterval, "ms");
|
||||
} catch (err) {
|
||||
console.warn("Failed to load /api/config:", err);
|
||||
console.warn("Failed to read firehose interval:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,19 +212,12 @@ async function fetchUpdates() {
|
||||
const fromNodeId = pkt.from_node_id;
|
||||
const toNodeId = pkt.to_node_id;
|
||||
|
||||
let from = nodeName(fromNodeId);
|
||||
if (fromNodeId !== 4294967295 && fromNodeId !== 1) {
|
||||
from = `<a href="/packet_list/${fromNodeId}" style="text-decoration:underline; color:inherit;">${nodeMap[fromNodeId]}</a>`;
|
||||
}
|
||||
let from = fromNodeId === 4294967295 ? `<span class="to-mqtt">All</span>` :
|
||||
`<a href="/packet_list/${fromNodeId}" style="text-decoration:underline; color:inherit;">${nodeMap[fromNodeId] || fromNodeId}</a>`;
|
||||
|
||||
let to = "";
|
||||
if (toNodeId === 1) {
|
||||
to = `<span class="to-mqtt">direct to MQTT</span>`;
|
||||
} else if (toNodeId === 4294967295) {
|
||||
to = `<span class="to-mqtt">All</span>`;
|
||||
} else {
|
||||
to = `<a href="/packet_list/${toNodeId}" style="text-decoration:underline; color:inherit;">${nodeMap[toNodeId]}</a>`;
|
||||
}
|
||||
let to = toNodeId === 1 ? `<span class="to-mqtt">direct to MQTT</span>` :
|
||||
toNodeId === 4294967295 ? `<span class="to-mqtt">All</span>` :
|
||||
`<a href="/packet_list/${toNodeId}" style="text-decoration:underline; color:inherit;">${nodeMap[toNodeId] || toNodeId}</a>`;
|
||||
|
||||
let links = "";
|
||||
if (pkt.portnum === 3 && pkt.payload) {
|
||||
@@ -248,7 +239,6 @@ async function fetchUpdates() {
|
||||
}
|
||||
|
||||
const safePayload = (pkt.payload || "").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
const html = `
|
||||
<tr class="packet-row" data-id="${pkt.id}">
|
||||
<td><span class="toggle-btn">▶</span> <a href="/packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
||||
@@ -260,7 +250,6 @@ async function fetchUpdates() {
|
||||
<tr class="payload-row">
|
||||
<td colspan="5" class="payload-cell">${safePayload}</td>
|
||||
</tr>`;
|
||||
|
||||
list.insertAdjacentHTML("afterbegin", html);
|
||||
}
|
||||
|
||||
@@ -288,10 +277,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
btn.textContent = row.classList.contains("expanded") ? "▼" : "▶";
|
||||
});
|
||||
|
||||
await loadConfig();
|
||||
await configureFirehose();
|
||||
await loadNodes();
|
||||
fetchUpdates();
|
||||
setInterval(fetchUpdates, updateInterval);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -59,11 +59,28 @@ function timeAgo(date){ const diff=Date.now()-new Date(date), s=Math.floor(diff/
|
||||
function hashToColor(str){ if(colorMap.has(str)) return colorMap.get(str); const c=palette[nextColorIndex++%palette.length]; colorMap.set(str,c); return c; }
|
||||
function isInvalidCoord(n){ return !n||!n.lat||!n.long||n.lat===0||n.long===0||Number.isNaN(n.lat)||Number.isNaN(n.long); }
|
||||
|
||||
|
||||
// ---------------------- WAIT FOR CONFIG ----------------------
|
||||
async function waitForConfig() {
|
||||
while (typeof window._siteConfigPromise === "undefined") {
|
||||
console.log("Waiting for _siteConfigPromise...");
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
try {
|
||||
const cfg = await window._siteConfigPromise;
|
||||
if (!cfg || !cfg.site) throw new Error("Config missing site object");
|
||||
return cfg.site;
|
||||
} catch (err) {
|
||||
console.error("Error loading site config:", err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Load Config & Start Polling ----------------------
|
||||
async function initMapPolling() {
|
||||
try {
|
||||
const cfg = await fetch('/api/config').then(r => r.json());
|
||||
const site = cfg.site || {};
|
||||
const site = await waitForConfig();
|
||||
mapInterval = parseInt(site.map_interval, 10) || 0;
|
||||
|
||||
// ---- Check URL params ----
|
||||
|
||||
+4
-34
@@ -1114,38 +1114,10 @@ async def net(request):
|
||||
|
||||
@routes.get("/map")
|
||||
async def map(request):
|
||||
try:
|
||||
# Parse optional URL parameters for custom view
|
||||
map_center_lat = request.query.get("lat")
|
||||
map_center_lng = request.query.get("lng")
|
||||
map_zoom = request.query.get("zoom")
|
||||
|
||||
# Validate and convert parameters if provided
|
||||
custom_view = None
|
||||
if map_center_lat and map_center_lng:
|
||||
try:
|
||||
lat = float(map_center_lat)
|
||||
lng = float(map_center_lng)
|
||||
zoom = int(map_zoom) if map_zoom else 13
|
||||
custom_view = {"lat": lat, "lng": lng, "zoom": zoom}
|
||||
except (ValueError, TypeError):
|
||||
# Invalid parameters, ignore and use defaults
|
||||
pass
|
||||
|
||||
template = env.get_template("map.html")
|
||||
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
custom_view=custom_view,
|
||||
),
|
||||
content_type="text/html",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"/map route error: {e}")
|
||||
template = env.get_template("map.html")
|
||||
return web.Response(
|
||||
text="An error occurred while processing your request.",
|
||||
status=500,
|
||||
content_type="text/plain",
|
||||
text=template.render(),
|
||||
content_type="text/html"
|
||||
)
|
||||
|
||||
|
||||
@@ -1257,7 +1229,7 @@ async def chat(request):
|
||||
try:
|
||||
template = env.get_template("chat.html")
|
||||
return web.Response(
|
||||
text=template.render(site_config=CONFIG, SOFTWARE_RELEASE=SOFTWARE_RELEASE),
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -1266,8 +1238,6 @@ async def chat(request):
|
||||
rendered = template.render(
|
||||
error_message="An error occurred while processing your request.",
|
||||
error_details=traceback.format_exc(),
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
)
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user