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:
Pablo Revilla
2025-11-05 20:46:12 -08:00
parent 4a3f205d26
commit ac4ac9264f
3 changed files with 201 additions and 154 deletions
+154 -57
View File
@@ -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}&section=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}&section=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
View File
@@ -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
View File
@@ -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]