worked on making map and base all API driven

This commit is contained in:
Pablo Revilla
2025-10-22 14:31:07 -07:00
parent 13aa73e88f
commit d9b1d5ac49
5 changed files with 114 additions and 192 deletions
+56 -99
View File
@@ -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}&section=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 += `&nbsp;-&nbsp;<a href="/chat">Conversations</a>`;
if (site.everything === "true") html += `&nbsp;-&nbsp;<a href="/firehose">See Everything</a>`;
if (site.graphs === "true") html += `&nbsp;-&nbsp;<a href="/nodegraph">Mesh Graphs</a>`;
if (site.net === "true") html += `&nbsp;-&nbsp;<a href="/net">Weekly Net</a>`;
if (site.map === "true") html += `&nbsp;-&nbsp;<a href="/map">Live Map</a>`;
if (site.stats === "true") html += `&nbsp;-&nbsp;<a href="/stats">Stats</a>`;
if (site.top === "true") html += `&nbsp;-&nbsp;<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 += `&nbsp;-&nbsp;<a href="/chat">Conversations</a>`;
if (site.everything === "true") html += `&nbsp;-&nbsp;<a href="/firehose">See Everything</a>`;
if (site.graphs === "true") html += `&nbsp;-&nbsp;<a href="/nodegraph">Mesh Graphs</a>`;
if (site.net === "true") html += `&nbsp;-&nbsp;<a href="/net">Weekly Net</a>`;
if (site.map === "true") html += `&nbsp;-&nbsp;<a href="/map">Live Map</a>`;
if (site.stats === "true") html += `&nbsp;-&nbsp;<a href="/stats">Stats</a>`;
if (site.top === "true") html += `&nbsp;-&nbsp;<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>
+23 -35
View File
@@ -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}&section=chat`);
chatTranslations = await res.json();
applyTranslations(chatTranslations, document);
+12 -22
View File
@@ -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, "&lt;").replace(/>/g, "&gt;");
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 %}
+19 -2
View File
@@ -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
View File
@@ -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")