mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-07-01 23:41:19 +02:00
Worked on /api/packet. Needed to modify
- Store.py to read the new time data - api.py to present the new time data - firehose.html chat.html and map.html now use the new apis and the time is the browser local time
This commit is contained in:
+154
-57
@@ -1,75 +1,172 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block css %}
|
||||
.timestamp {
|
||||
min-width:10em;
|
||||
}
|
||||
.chat-packet:nth-of-type(odd){
|
||||
background-color: #3a3a3a; /* Lighter than #2a2a2a */
|
||||
}
|
||||
.timestamp { min-width: 10em; color: #ccc; }
|
||||
|
||||
.chat-packet:nth-of-type(odd) { background-color: #3a3a3a; }
|
||||
.chat-packet {
|
||||
border-bottom: 1px solid #555;
|
||||
padding: 8px;
|
||||
border-radius: 8px; /* Adjust the value to make the corners more or less rounded */
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
.chat-packet:nth-of-type(even){
|
||||
background-color: #333333; /* Slightly lighter than the previous #181818 */
|
||||
|
||||
.chat-packet > [class^="col-"] {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
padding-top: 1px !important;
|
||||
padding-bottom: 1px !important;
|
||||
}
|
||||
|
||||
.chat-packet:nth-of-type(even) { background-color: #333333; }
|
||||
|
||||
.channel { font-style: italic; color: #bbb; }
|
||||
.channel a { font-style: normal; color: #999; }
|
||||
|
||||
@keyframes flash { 0% { background-color: #ffe066; } 100% { background-color: inherit; } }
|
||||
.chat-packet.flash { animation: flash 3.5s ease-out; }
|
||||
|
||||
.replying-to { font-size: 0.8em; color: #aaa; margin-top: 2px; padding-left: 10px; }
|
||||
.replying-to .reply-preview { color: #aaa; }
|
||||
|
||||
#weekly-message { margin: 15px 0; font-weight: bold; color: #ffeb3b; }
|
||||
#total-count { margin-bottom: 10px; font-style: italic; color: #ccc; }
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<span>{{ site_config["site"]["weekly_net_message"] }}</span> <br><br>
|
||||
<!-- Weekly notice -->
|
||||
<div id="weekly-message">Loading weekly message...</div>
|
||||
<!-- Total messages -->
|
||||
<div id="total-count">Total messages: 0</div>
|
||||
|
||||
<h5>
|
||||
<span data-translate-lang="number_of_checkins">Number of Check-ins:</span> {{ packets|length }}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% for packet in packets %}
|
||||
<div
|
||||
class="row chat-packet"
|
||||
data-packet-id="{{ packet.id }}"
|
||||
role="article"
|
||||
aria-label="Chat message from {{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}"
|
||||
>
|
||||
<span class="col-2 timestamp">
|
||||
{{ packet.import_time.strftime('%-I:%M:%S %p - %m-%d-%Y') }}
|
||||
</span>
|
||||
<span class="col-1 timestamp">
|
||||
<a href="/packet/{{ packet.id }}" title="View packet details">✉️</a> {{ packet.from_node.channel }}
|
||||
</span>
|
||||
<span class="col-2 username">
|
||||
<a href="/packet_list/{{ packet.from_node_id }}" title="View all packets from this node">
|
||||
{{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="col-5 message">
|
||||
{{ packet.payload }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span data-translate-lang="no_packets_found">No packets found.</span>
|
||||
{% endfor %}
|
||||
<div id="chat-container">
|
||||
<div class="container" id="chat-log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
const langCode = "{{ site_config.get('site', {}).get('language','en') }}";
|
||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=net`);
|
||||
const translations = await res.json();
|
||||
document.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
const key = el.dataset.translateLang;
|
||||
if(el.placeholder !== undefined && el.placeholder !== "") el.placeholder = translations[key] || el.placeholder;
|
||||
else el.textContent = translations[key] || el.textContent;
|
||||
});
|
||||
} catch(err) {
|
||||
console.error("Net translations load failed:", err);
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const chatContainer = document.querySelector("#chat-log");
|
||||
const totalCountEl = document.querySelector("#total-count");
|
||||
const weeklyMessageEl = document.querySelector("#weekly-message");
|
||||
if (!chatContainer || !totalCountEl || !weeklyMessageEl) return console.error("Required elements not found");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadTranslations());
|
||||
const renderedPacketIds = new Set();
|
||||
const packetMap = new Map();
|
||||
let chatTranslations = {};
|
||||
let netTag = "";
|
||||
|
||||
// Fetch site config to get net_tag and weekly message
|
||||
try {
|
||||
const resp = await fetch("/api/config");
|
||||
const config = await resp.json();
|
||||
netTag = encodeURIComponent(config?.site?.net_tag || "");
|
||||
weeklyMessageEl.textContent = config?.site?.weekly_net_message || "Weekly message not set.";
|
||||
} catch(err) {
|
||||
console.error("Failed to load site config:", err);
|
||||
weeklyMessageEl.textContent = "Failed to load weekly message.";
|
||||
}
|
||||
|
||||
function updateTotalCount() {
|
||||
totalCountEl.textContent = `Total messages: ${renderedPacketIds.size}`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text ?? "";
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function applyTranslations(translations, root=document) {
|
||||
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
const key = el.dataset.translateLang;
|
||||
if (translations[key]) el.textContent = translations[key];
|
||||
});
|
||||
root.querySelectorAll("[data-translate-lang-title]").forEach(el => {
|
||||
const key = el.dataset.translateLangTitle;
|
||||
if (translations[key]) el.title = translations[key];
|
||||
});
|
||||
}
|
||||
|
||||
function renderPacket(packet) {
|
||||
if (renderedPacketIds.has(packet.id)) return;
|
||||
renderedPacketIds.add(packet.id);
|
||||
packetMap.set(packet.id, packet);
|
||||
|
||||
const date = new Date(packet.import_time_us / 1000);
|
||||
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}`;
|
||||
|
||||
let replyHtml = "";
|
||||
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">
|
||||
<i data-translate-lang="replying_to"></i>
|
||||
<a href="/packet/${packet.reply_id}">${packet.reply_id}</a>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "row chat-packet";
|
||||
div.dataset.packetId = packet.id;
|
||||
div.innerHTML = `
|
||||
<span class="col-2 timestamp" title="${packet.import_time_us}">${formattedTimestamp}</span>
|
||||
<span class="col-2 channel">
|
||||
<a href="/packet/${packet.id}" data-translate-lang-title="view_packet_details">✉️</a>
|
||||
${escapeHtml(packet.channel || "")}
|
||||
</span>
|
||||
<span class="col-3 nodename">
|
||||
<a href="/packet_list/${packet.from_node_id}">
|
||||
${escapeHtml((packet.long_name || "").trim() || `Node ${packet.from_node_id}`)}
|
||||
</a>
|
||||
</span>
|
||||
<span class="col-5 message">${escapeHtml(packet.payload)}${replyHtml}</span>
|
||||
`;
|
||||
chatContainer.prepend(div);
|
||||
applyTranslations(chatTranslations, div);
|
||||
updateTotalCount();
|
||||
}
|
||||
|
||||
function renderPacketsEnsureDescending(packets) {
|
||||
if (!Array.isArray(packets) || packets.length===0) return;
|
||||
const sortedDesc = packets.slice().sort((a,b)=>b.import_time_us - a.import_time_us);
|
||||
for (let i=sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i]);
|
||||
}
|
||||
|
||||
async function fetchInitial() {
|
||||
try {
|
||||
if (!netTag) return;
|
||||
const resp = await fetch(`/api/packets?portnum=1&limit=100&contains=${netTag}`);
|
||||
const data = await resp.json();
|
||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets);
|
||||
} catch(err) { console.error("Initial fetch error:", err); }
|
||||
}
|
||||
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
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);
|
||||
} catch(err){ console.error("Chat translation load failed:", err); }
|
||||
}
|
||||
|
||||
await loadTranslations();
|
||||
await fetchInitial();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+37
-95
@@ -210,6 +210,43 @@ async def index(request):
|
||||
starting_url = CONFIG["site"].get("starting", "/map") # default to /map if not set
|
||||
raise web.HTTPFound(location=starting_url)
|
||||
|
||||
@routes.get("/net")
|
||||
async def net(request):
|
||||
return web.Response(
|
||||
text=env.get_template("net.html").render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
@routes.get("/map")
|
||||
async def map(request):
|
||||
template = env.get_template("map.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html"
|
||||
)
|
||||
|
||||
@routes.get("/nodelist")
|
||||
async def nodelist(request):
|
||||
template = env.get_template("nodelist.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
@routes.get("/firehose")
|
||||
async def firehose(request):
|
||||
return web.Response(
|
||||
text=env.get_template("firehose.html").render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
@routes.get("/chat")
|
||||
async def chat(request):
|
||||
template = env.get_template("chat.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
def generate_response(request, body, raw_node_id="", node=None):
|
||||
if "HX-Request" in request.headers:
|
||||
@@ -434,14 +471,6 @@ async def packet_details(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/firehose")
|
||||
async def packet_details_firehose(request):
|
||||
return web.Response(
|
||||
text=env.get_template("firehose.html").render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/packet/{packet_id}")
|
||||
async def packet(request):
|
||||
try:
|
||||
@@ -1069,75 +1098,6 @@ async def graph_network(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/nodelist")
|
||||
async def nodelist(request):
|
||||
try:
|
||||
template = env.get_template("nodelist.html")
|
||||
return web.Response(
|
||||
text=template.render(site_config=CONFIG, SOFTWARE_RELEASE=SOFTWARE_RELEASE),
|
||||
content_type="text/html",
|
||||
)
|
||||
except Exception:
|
||||
template = env.get_template("error.html")
|
||||
rendered = template.render(
|
||||
error_message="An error occurred while loading the node list page.",
|
||||
error_details=traceback.format_exc(),
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
)
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
|
||||
|
||||
@routes.get("/net")
|
||||
async def net(request):
|
||||
try:
|
||||
# Fetch packets for the given node ID and port number
|
||||
after_time = datetime.datetime.now() - timedelta(days=6)
|
||||
packets = await store.get_packets(portnum=PortNum.TEXT_MESSAGE_APP, after=after_time)
|
||||
|
||||
# Convert packets to UI packets
|
||||
ui_packets = [Packet.from_model(p) for p in packets]
|
||||
# Precompile regex for performance
|
||||
seq_pattern = re.compile(r"seq \d+$")
|
||||
|
||||
# Filter packets: exclude "seq \d+$" but include those containing Tag
|
||||
filtered_packets = [
|
||||
p
|
||||
for p in ui_packets
|
||||
if not seq_pattern.match(p.payload)
|
||||
and (CONFIG["site"]["net_tag"]).lower() in p.payload.lower()
|
||||
]
|
||||
|
||||
# Render template
|
||||
template = env.get_template("net.html")
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
packets=filtered_packets, site_config=CONFIG, SOFTWARE_RELEASE=SOFTWARE_RELEASE
|
||||
),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
except web.HTTPException:
|
||||
raise # Let aiohttp handle HTTP exceptions properly
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing net request: {e}")
|
||||
template = env.get_template("error.html")
|
||||
rendered = template.render(
|
||||
error_message="An internal server error occurred.",
|
||||
error_details=traceback.format_exc(),
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
)
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
|
||||
|
||||
@routes.get("/map")
|
||||
async def map(request):
|
||||
template = env.get_template("map.html")
|
||||
return web.Response(text=template.render(), content_type="text/html")
|
||||
|
||||
|
||||
@routes.get("/stats")
|
||||
async def stats(request):
|
||||
try:
|
||||
@@ -1240,24 +1200,6 @@ async def top(request):
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
|
||||
|
||||
@routes.get("/chat")
|
||||
async def chat(request):
|
||||
try:
|
||||
template = env.get_template("chat.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /chat: {e}")
|
||||
template = env.get_template("error.html")
|
||||
rendered = template.render(
|
||||
error_message="An error occurred while processing your request.",
|
||||
error_details=traceback.format_exc(),
|
||||
)
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
|
||||
|
||||
# Assuming the route URL structure is /nodegraph
|
||||
@routes.get("/nodegraph")
|
||||
async def nodegraph(request):
|
||||
|
||||
+10
-2
@@ -97,6 +97,7 @@ async def api_packets(request):
|
||||
limit_str = request.query.get("limit", "50")
|
||||
since_str = request.query.get("since")
|
||||
portnum = request.query.get("portnum")
|
||||
contains = request.query.get("contains") # <-- new query parameter
|
||||
|
||||
# Clamp limit between 1 and 100
|
||||
try:
|
||||
@@ -126,10 +127,17 @@ async def api_packets(request):
|
||||
if str(portnum) == str(PortNum.TEXT_MESSAGE_APP):
|
||||
# Filter out empty or "seq N" payloads
|
||||
ui_packets = [
|
||||
p for p in ui_packets if p.payload and not SEQ_REGEX.fullmatch(p.payload)
|
||||
p for p in ui_packets
|
||||
if p.payload and not SEQ_REGEX.fullmatch(p.payload)
|
||||
]
|
||||
|
||||
# Sort newest first
|
||||
# Apply "contains" filter if provided
|
||||
if contains:
|
||||
ui_packets = [
|
||||
p for p in ui_packets if contains.lower() in p.payload.lower()
|
||||
]
|
||||
|
||||
# Sort newest first and limit
|
||||
ui_packets.sort(key=lambda p: p.import_time_us, reverse=True)
|
||||
ui_packets = ui_packets[:limit]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user