Fixed Sort nodes by firmware in nodelist.html

This commit is contained in:
Pablo Revilla
2025-12-01 09:38:08 -08:00
parent 2149fed8c5
commit fd653f8234
10 changed files with 308 additions and 1044 deletions
-100
View File
@@ -1,100 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Documentation - Config</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
<style>
body { margin: 0; background: #ffffff; color: #000; }
#swagger-ui { background: #ffffff; color: #000; }
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
.swagger-ui .topbar,
.swagger-ui .info,
.swagger-ui .opblock-summary-description,
.swagger-ui .parameters-col_description,
.swagger-ui .response-col_description { color: #000 !important; }
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
.swagger-ui .opblock-section-header { color: #000 !important; }
.swagger-ui .parameters,
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
.swagger-ui table { color: #000 !important; }
.swagger-ui a { color: #1a0dab !important; }
.swagger-ui input,
.swagger-ui select,
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
const spec = {
openapi: "3.0.0",
info: {
title: "Site Config API",
version: "1.0.0",
description: "API for retrieving the site configuration. This endpoint does not take any parameters."
},
paths: {
"/api/config": {
get: {
summary: "Get site configuration",
description: "Returns the current site configuration object.",
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
site_config: {
type: "object",
additionalProperties: true,
example: {
site_name: "MeshView",
firehose_interval: 1000,
starting: "/nodes",
theme: "dark"
}
}
}
}
}
}
},
"500": {
description: "Server error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: { type: "string", example: "Internal server error" }
}
}
}
}
}
}
}
}
}
};
window.onload = () => {
SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
-109
View File
@@ -1,109 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Documentation - Edges</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
<style>
body {
margin: 0;
background: #ffffff;
color: #000000;
}
#swagger-ui { background: #ffffff; color: #000; }
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
.swagger-ui .topbar,
.swagger-ui .info,
.swagger-ui .opblock-summary-description,
.swagger-ui .parameters-col_description,
.swagger-ui .response-col_description { color: #000 !important; }
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
.swagger-ui .opblock-section-header { color: #000 !important; }
.swagger-ui .parameters,
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
.swagger-ui table { color: #000 !important; }
.swagger-ui a { color: #1a0dab !important; }
.swagger-ui input,
.swagger-ui select,
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
const spec = {
openapi: "3.0.0",
info: {
title: "Network Edges API",
version: "1.0.0",
description: "API for retrieving network edges derived from traceroutes and neighbor info packets, with optional type filtering."
},
paths: {
"/api/edges": {
get: {
summary: "Get network edges",
description: "Returns edges between nodes in the network. Optionally filter by type (`traceroute` or `neighbor`).",
parameters: [
{
name: "type",
in: "query",
required: false,
description: "Optional filter to only return edges of this type (`traceroute` or `neighbor`).",
schema: { type: "string", enum: ["traceroute", "neighbor"] }
}
],
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
edges: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "integer", example: 101 },
to: { type: "integer", example: 102 },
type: { type: "string", example: "traceroute" }
}
}
}
}
}
}
}
},
"400": {
description: "Invalid request parameters",
content: {
"application/json": {
schema: { type: "object", properties: { error: { type: "string" } } }
}
}
}
}
}
}
}
};
window.onload = () => {
SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
-134
View File
@@ -1,134 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Documentation - Nodes</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
<style>
body { margin: 0; background: #ffffff; color: #000; }
#swagger-ui { background: #ffffff; color: #000; }
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
.swagger-ui .topbar,
.swagger-ui .info,
.swagger-ui .opblock-summary-description,
.swagger-ui .parameters-col_description,
.swagger-ui .response-col_description { color: #000 !important; }
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
.swagger-ui .opblock-section-header { color: #000 !important; }
.swagger-ui .parameters,
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
.swagger-ui table { color: #000 !important; }
.swagger-ui a { color: #1a0dab !important; }
.swagger-ui input,
.swagger-ui select,
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
const spec = {
openapi: "3.0.0",
info: {
title: "Network Nodes API",
version: "1.0.0",
description: "API for retrieving nodes in the network with optional filters by last seen time."
},
paths: {
"/api/nodes": {
get: {
summary: "Get network nodes",
description: "Returns a list of nodes with optional filtering by recent activity.",
parameters: [
{
name: "hours",
in: "query",
required: false,
description: "Return nodes seen in the last X hours.",
schema: { type: "integer", example: 24 }
},
{
name: "days",
in: "query",
required: false,
description: "Return nodes seen in the last X days.",
schema: { type: "integer", example: 7 }
},
{
name: "last_seen_after",
in: "query",
required: false,
description: "Return nodes last seen after this ISO8601 timestamp.",
schema: { type: "string", format: "date-time", example: "2025-08-25T14:00:00" }
}
],
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
nodes: {
type: "array",
items: {
type: "object",
properties: {
node_id: { type: "integer", example: 101 },
long_name: { type: "string", example: "Node Alpha" },
short_name: { type: "string", example: "A" },
channel: { type: "string", example: "2" },
last_seen: { type: "string", format: "date-time", example: "2025-08-25T12:00:00" },
last_lat: { type: "number", format: "float", example: 37.7749 },
last_long: { type: "number", format: "float", example: -122.4194 },
hardware: { type: "string", example: "Heltec V3" },
firmware: { type: "string", example: "1.0.5" },
role: { type: "string", example: "router" }
}
}
}
}
}
}
}
},
"400": {
description: "Invalid request parameters",
content: {
"application/json": {
schema: { type: "object", properties: { error: { type: "string" } } }
}
}
},
"500": {
description: "Server error",
content: {
"application/json": {
schema: { type: "object", properties: { error: { type: "string" } } }
}
}
}
}
}
}
}
};
window.onload = () => {
SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
-167
View File
@@ -1,167 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Documentation - Packets</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
<style>
body {
margin: 0;
background: #ffffff;
color: #000000;
}
#swagger-ui {
background: #ffffff;
color: #000000;
}
/* Override Swagger UI colors for white background */
.swagger-ui {
background-color: #ffffff !important;
color: #000000 !important;
}
.swagger-ui .topbar,
.swagger-ui .info,
.swagger-ui .opblock-summary-description,
.swagger-ui .parameters-col_description,
.swagger-ui .response-col_description {
color: #000000 !important;
}
.swagger-ui .opblock {
background-color: #f9f9f9 !important;
border-color: #ddd !important;
}
.swagger-ui .opblock-summary {
background-color: #eaeaea !important;
color: #000 !important;
}
.swagger-ui .opblock-section-header {
color: #000 !important;
}
.swagger-ui .parameters,
.swagger-ui .response {
background-color: #fafafa !important;
color: #000 !important;
}
.swagger-ui table {
color: #000 !important;
}
.swagger-ui a {
color: #1a0dab !important; /* classic link blue */
}
.swagger-ui input,
.swagger-ui select,
.swagger-ui textarea {
background-color: #fff !important;
color: #000 !important;
border: 1px solid #ccc !important;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
const spec = {
openapi: "3.0.0",
info: {
title: "Packets API",
version: "1.0.0",
description: "API for retrieving packet records with optional filters."
},
paths: {
"/api/packets": {
get: {
summary: "Get packets",
description: "Returns a list of recent packets, optionally filtered by a timestamp and limited by count.",
parameters: [
{
name: "limit",
in: "query",
required: false,
description: "Maximum number of packets to return. Default is 200.",
schema: {
type: "integer",
default: 200
}
},
{
name: "since",
in: "query",
required: false,
description: "Only return packets imported after this ISO8601 timestamp (e.g., `2025-08-12T14:15:20`).",
schema: {
type: "string",
format: "date-time"
}
}
],
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
packets: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer", example: 196988973 },
from_node_id: { type: "integer", example: 2381019191 },
to_node_id: { type: "integer", example: 1234567890 },
portnum: { type: "integer", example: 1 },
import_time: { type: "string", format: "date-time", example: "2025-08-12T14:15:20.503827" },
payload: { type: "string", example: "Hello Mesh" }
}
}
}
}
}
}
}
},
"500": {
description: "Internal server error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: { type: "string", example: "Failed to fetch packets" }
}
}
}
}
}
}
}
}
}
};
window.onload = () => {
SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
-210
View File
@@ -1,210 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Documentation - Packet Stats</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
<style>
body {
margin: 0;
background: #ffffff;
color: #000000;
}
#swagger-ui {
background: #ffffff;
color: #000000;
}
/* Override Swagger UI colors for white background */
.swagger-ui {
background-color: #ffffff !important;
color: #000000 !important;
}
.swagger-ui .topbar,
.swagger-ui .info,
.swagger-ui .opblock-summary-description,
.swagger-ui .parameters-col_description,
.swagger-ui .response-col_description {
color: #000000 !important;
}
.swagger-ui .opblock {
background-color: #f9f9f9 !important;
border-color: #ddd !important;
}
.swagger-ui .opblock-summary {
background-color: #eaeaea !important;
color: #000 !important;
}
.swagger-ui .opblock-section-header {
color: #000 !important;
}
.swagger-ui .parameters,
.swagger-ui .response {
background-color: #fafafa !important;
color: #000 !important;
}
.swagger-ui table {
color: #000 !important;
}
.swagger-ui a {
color: #1a0dab !important; /* classic link blue */
}
.swagger-ui input,
.swagger-ui select,
.swagger-ui textarea {
background-color: #fff !important;
color: #000 !important;
border: 1px solid #ccc !important;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
const spec = {
openapi: "3.0.0",
info: {
title: "Packet Statistics API",
version: "1.0.0",
description: "API for retrieving packet statistics over a given period with optional filters."
},
paths: {
"/api/stats": {
get: {
summary: "Get packet statistics",
description: "Returns packet statistics for a given period type and length, with optional filters.",
parameters: [
{
name: "period_type",
in: "query",
required: false,
description: "Type of period to group by (`hour` or `day`). Default is `hour`.",
schema: {
type: "string",
enum: ["hour", "day"]
}
},
{
name: "length",
in: "query",
required: false,
description: "Number of periods to include. Default is 24.",
schema: {
type: "integer",
default: 24
}
},
{
name: "channel",
in: "query",
required: false,
description: "Filter by channel name.",
schema: {
type: "string"
}
},
{
name: "portnum",
in: "query",
required: false,
description: "Filter by port number.",
schema: {
type: "integer"
}
},
{
name: "to_node",
in: "query",
required: false,
description: "Filter by destination node ID.",
schema: {
type: "integer"
}
},
{
name: "from_node",
in: "query",
required: false,
description: "Filter by source node ID.",
schema: {
type: "integer"
}
}
],
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
hourly: {
type: "object",
properties: {
period_type: { type: "string" },
length: { type: "integer" },
filters: { type: "object" },
data: {
type: "array",
items: {
type: "object",
properties: {
period: { type: "string", example: "2025-08-06 19:00" },
node_id: { type: "integer" },
long_name: { type: "string" },
short_name: { type: "string" },
packets: { type: "integer" }
}
}
}
}
}
}
}
}
}
},
"400": {
description: "Invalid request parameters",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: { type: "string" }
}
}
}
}
}
}
}
}
}
};
window.onload = () => {
SwaggerUIBundle({
spec,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
+254 -72
View File
@@ -1,91 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API Index</title>
<style>
<meta charset="utf-8">
<title>Meshview API Documentation</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #1e1e1e;
color: #eaeaea;
margin: 0;
padding: 0;
background: #121212;
color: #eee;
font-family: monospace;
margin: 20px;
line-height: 1.5;
}
header {
background: #2a2a2a;
padding: 20px;
text-align: center;
font-size: 1.6em;
font-weight: bold;
h1, h2, h3 { color: #79c0ff; }
code {
background: #1e1e1e;
padding: 3px 6px;
border-radius: 4px;
color: #ffd479;
font-size: 0.95rem;
}
.container {
max-width: 800px;
margin: 30px auto;
padding: 20px;
.endpoint {
border: 1px solid #333;
padding: 12px;
margin-bottom: 18px;
border-radius: 8px;
background: #1a1a1a;
}
ul {
list-style: none;
padding: 0;
.method {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
margin-right: 6px;
font-weight: bold;
}
li {
background: #272b2f;
border: 1px solid #474b4e;
padding: 15px 20px;
margin-bottom: 15px;
border-radius: 10px;
transition: background 0.2s;
.get { background: #0066cc; }
.path { font-weight: bold; color: #fff; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
li:hover {
background: #33383d;
th, td {
border: 1px solid #444;
padding: 6px 10px;
}
a {
color: #4cafef;
text-decoration: none;
font-weight: bold;
font-size: 1.1em;
th {
background: #222;
color: #9ddcff;
}
p {
margin: 8px 0 0 0;
font-size: 0.9em;
color: #bbbbbb;
.example {
margin-top: 10px;
padding: 10px;
background: #161616;
border-radius: 6px;
border: 1px solid #333;
}
</style>
</style>
</head>
<body>
<header>
API Index
</header>
<h1>Meshview API Documentation</h1>
<p>This page describes all REST endpoints provided by Meshview.</p>
<div class="container">
<ul>
<li>
<a href="/api-chat">Chat API</a>
<p> View chat messages.</p>
</li>
<li>
<a href="/api-nodes">Node API</a>
<p>Retrieve node information.</p>
</li>
<li>
<a href="/api-packets">Packet API</a>
<p>Access raw packet data.</p>
</li>
<li>
<a href="/api-stats">Statistics API </a>
<p>View system and traffic statistics.</p>
</li>
<li>
<a href="/api-edges">Edges API</a>
<p>Get edges details.</p>
</li>
<li>
<a href="/api-config">Configuration API</a>
<p>Get and update configuration details.</p>
</li>
</ul>
<!------------------------------ NODES ------------------------------>
<h2>/api/nodes</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/nodes</span>
<p>Returns a list of mesh nodes.</p>
<h3>Query Parameters</h3>
<table>
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td>role</td><td>Filter by node role</td></tr>
<tr><td>channel</td><td>Filter by channel</td></tr>
<tr><td>hw_model</td><td>Hardware model filter</td></tr>
<tr><td>days_active</td><td>Only nodes seen within X days</td></tr>
</table>
<div class="example">
<b>Example:</b><br>
<code>/api/nodes?days_active=3</code>
</div>
</div>
<!------------------------------ PACKETS ------------------------------>
<h2>/api/packets</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/packets</span>
<p>Fetch packets with many filters. Returns decoded packet data.</p>
<h3>Query Parameters</h3>
<table>
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td>packet_id</td><td>Return exactly one packet</td></tr>
<tr><td>limit</td><td>Max number of results (1100)</td></tr>
<tr><td>since</td><td>Only packets newer than import_time_us</td></tr>
<tr><td>from_node_id</td><td>Filter by sender node</td></tr>
<tr><td>to_node_id</td><td>Filter by destination node</td></tr>
<tr><td>node_id</td><td>Legacy: match either from or to</td></tr>
<tr><td>portnum</td><td>Filter by port number</td></tr>
<tr><td>contains</td><td>Substring filter for payload</td></tr>
</table>
<div class="example">
<b>Example:</b><br>
<code>/api/packets?from_node_id=123&limit=100</code>
</div>
</div>
<!------------------------------ PACKETS SEEN ------------------------------>
<h2>/api/packets_seen/{packet_id}</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/packets_seen/&lt;packet_id&gt;</span>
<p>Returns list of gateways that heard the packet (RSSI/SNR/hops).</p>
<div class="example">
<b>Example:</b><br>
<code>/api/packets_seen/3314808102</code>
</div>
</div>
<!------------------------------ STATS ------------------------------>
<h2>/api/stats</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/stats</span>
<p>Returns aggregated packet statistics for a node or globally.</p>
<h3>Query Parameters</h3>
<table>
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td>period_type</td><td>"hour" or "day"</td></tr>
<tr><td>length</td><td>How many hours/days</td></tr>
<tr><td>node</td><td>Node ID for combined sent+seen stats</td></tr>
<tr><td>from_node</td><td>Filter by sender</td></tr>
<tr><td>to_node</td><td>Filter by receiver</td></tr>
<tr><td>portnum</td><td>Filter by port</td></tr>
<tr><td>channel</td><td>Filter by channel</td></tr>
</table>
<div class="example">
<b>Example:</b><br>
<code>/api/stats?node=1128180332&period_type=day&length=1</code>
</div>
</div>
<!------------------------------ STATS COUNT ------------------------------>
<h2>/api/stats/count</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/stats/count</span>
<p>
Returns <b>total packets</b> and <b>total packet_seen entries</b>.
When no filters are provided, it returns global totals.
When filters are specified, they narrow the time, channel,
direction, or specific packet.
</p>
<h3>Query Parameters</h3>
<table>
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td>period_type</td><td>"hour" or "day"</td></tr>
<tr><td>length</td><td>Number of hours or days (depends on period_type)</td></tr>
<tr><td>channel</td><td>Filter by channel</td></tr>
<tr><td>from_node</td><td>Only packets sent by this node</td></tr>
<tr><td>to_node</td><td>Only packets received by this node</td></tr>
<tr><td>packet_id</td><td>Filter seen counts for specific packet_id</td></tr>
</table>
<div class="example">
<b>Example:</b><br>
<code>/api/stats/count?from_node=1128180332&period_type=day&length=1</code>
</div>
</div>
<!------------------------------ EDGES ------------------------------>
<h2>/api/edges</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/edges</span>
<p>Returns traceroute and/or neighbor edges for graph rendering.</p>
<h3>Query Parameters</h3>
<table>
<tr><th>Parameter</th><th>Description</th></tr>
<tr><td>type</td><td>"traceroute", "neighbor", or omitted for both</td></tr>
</table>
<div class="example">
<b>Example:</b><br>
<code>/api/edges?type=neighbor</code>
</div>
</div>
<!------------------------------ CONFIG ------------------------------>
<h2>/api/config</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/config</span>
<p>Returns Meshview configuration (site, MQTT, cleanup, etc.).</p>
<div class="example">
<code>/api/config</code>
</div>
</div>
<!------------------------------ LANG ------------------------------>
<h2>/api/lang</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/api/lang</span>
<p>Returns translated text for the UI.</p>
<h3>Parameters</h3>
<table>
<tr><th>lang</th><td>Language code (e.g. "en")</td></tr>
<tr><th>section</th><td>Optional UI section (firehose, map, top...)</td></tr>
</table>
<div class="example">
<code>/api/lang?lang=en&section=firehose</code>
</div>
</div>
<!------------------------------ HEALTH ------------------------------>
<h2>/health</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/health</span>
<p>Returns API + database status.</p>
</div>
<!------------------------------ VERSION ------------------------------>
<h2>/version</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/version</span>
<p>Returns Meshview version and Git revision.</p>
</div>
<br><br>
<hr>
<p style="text-align:center; color:#666;">Meshview API — generated documentation</p>
</body>
</html>
+35 -16
View File
@@ -190,32 +190,51 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
activeBlinks.set(marker,interval);
}
let lastImportTime=null;
let lastImportTimeUs = null;
async function fetchLatestPacket(){
try{
const res=await fetch(`/api/packets?limit=1`);
const data=await res.json();
lastImportTime=data.packets?.[0]?.import_time || new Date().toISOString();
}catch(err){ console.error(err); }
const res = await fetch(`/api/packets?limit=1`);
const data = await res.json();
lastImportTimeUs = data.packets?.[0]?.import_time_us || 0;
}catch(err){
console.error(err);
}
}
async function fetchNewPackets(){
if(!lastImportTime) return;
if (!lastImportTimeUs) return;
try{
const res=await fetch(`/api/packets?since=${lastImportTime}`);
const data=await res.json();
const res = await fetch(`/api/packets?since=${lastImportTimeUs}`);
const data = await res.json();
if(!data.packets || !data.packets.length) return;
let latest=lastImportTime;
data.packets.forEach(packet=>{
if(packet.import_time && packet.import_time>latest) latest=packet.import_time;
const marker=markerById[packet.from_node_id];
const nodeData=nodeMap.get(packet.from_node_id);
if(marker && nodeData) blinkNode(marker,nodeData.long_name,packet.portnum);
let latest = lastImportTimeUs;
data.packets.forEach(packet => {
// Track newest microsecond timestamp
if (packet.import_time_us && packet.import_time_us > latest) {
latest = packet.import_time_us;
}
// Look up marker and blink it
const marker = markerById[packet.from_node_id];
const nodeData = nodeMap.get(packet.from_node_id);
if (marker && nodeData) {
blinkNode(marker, nodeData.long_name, packet.portnum);
}
});
lastImportTime=latest;
}catch(err){ console.error(err); }
lastImportTimeUs = latest;
}catch(err){
console.error(err);
}
}
if(mapInterval>0){ fetchLatestPacket(); setInterval(fetchNewPackets,mapInterval*1000); }
})();
-209
View File
@@ -1,209 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Mesh Nodes Live Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
body { margin: 0; }
#map { height: 100vh; width: 100%; }
#legend {
position: absolute; bottom: 10px; right: 10px;
background: rgba(0,0,0,0.7);
color: white; padding: 8px 12px;
font-family: monospace; font-size: 13px;
border-radius: 5px; z-index: 1000;
}
.legend-item { display: flex; align-items: center; margin-bottom: 4px; }
.legend-color { width: 16px; height: 16px; margin-right: 6px; border-radius: 4px; }
.pulse-label span {
background: rgba(0,0,0,0.6);
padding: 2px 4px;
border-radius: 3px;
pointer-events: none;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="legend"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map("map");
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(map);
const nodeMarkers = new Map();
let lastPacketTime = null;
const portColors = {
1:"red",
67:"cyan",
3:"orange",
70:"purple",
4:"yellow",
71:"brown",
73:"pink"
};
const portLabels = {
1:"Text",
67:"Telemetry",
3:"Position",
70:"Traceroute",
4:"Node Info",
71:"Neighbour Info",
73:"Map Report"
};
function getPulseColor(portnum) { return portColors[portnum] || "green"; }
// Legend
const legend = document.getElementById("legend");
for (const [port, color] of Object.entries(portColors)) {
const item = document.createElement("div");
item.className = "legend-item";
const colorBox = document.createElement("div");
colorBox.className = "legend-color";
colorBox.style.background = color;
const label = document.createElement("span");
label.textContent = `${portLabels[port] || "Custom"} (${port})`;
item.appendChild(colorBox);
item.appendChild(label);
legend.appendChild(item);
}
// Pulse marker
function pulseMarker(marker, highlightColor = "red") {
if (!marker || marker.activePulse) return;
marker.activePulse = true;
const originalColor = marker.options.originalColor;
const originalRadius = marker.options.originalRadius;
marker.bringToFront();
const nodeInfo = marker.options.nodeInfo || {};
const portLabel = marker.currentPortLabel || "";
const displayName = `${nodeInfo.long_name || nodeInfo.short_name || "Unknown"}${portLabel ? ` (<i>${portLabel}</i>)` : ""}`;
marker.bindTooltip(displayName, {
permanent: true,
direction: 'top',
className: 'pulse-label',
offset: [0, -10],
html: true
}).openTooltip();
const flashDuration = 2000, fadeDuration = 1000, flashInterval = 100, maxRadius = originalRadius + 5;
let flashTime = 0;
const flashTimer = setInterval(() => {
flashTime += flashInterval;
const isOn = (flashTime / flashInterval) % 2 === 0;
marker.setStyle({ fillColor: isOn ? highlightColor : originalColor, radius: isOn ? maxRadius : originalRadius });
if (flashTime >= flashDuration) {
clearInterval(flashTimer);
const fadeStart = performance.now();
function fade(now) {
const t = Math.min((now - fadeStart) / fadeDuration, 1);
const radius = originalRadius + (maxRadius - originalRadius) * (1 - t);
marker.setStyle({ fillColor: highlightColor, radius: radius, fillOpacity: 1 });
if (t < 1) requestAnimationFrame(fade);
else {
marker.setStyle({ fillColor: originalColor, radius: originalRadius, fillOpacity: 1 });
marker.unbindTooltip();
marker.activePulse = false;
}
}
requestAnimationFrame(fade);
}
}, flashInterval);
}
// --- Load nodes ---
async function loadNodes() {
try {
const res = await fetch("/api/nodes");
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
const data = await res.json();
const nodes = data.nodes || [];
nodes.forEach(node => {
const lat = node.last_lat / 1e7;
const lng = node.last_long / 1e7;
if (lat && lng && !isNaN(lat) && !isNaN(lng)) {
const color = "blue";
const marker = L.circleMarker([lat,lng], {
radius:7, color:"white", fillColor:color, fillOpacity:1, weight:0.7
}).addTo(map);
marker.options.originalColor = color;
marker.options.originalRadius = 7;
marker.options.nodeInfo = node;
marker.bindPopup(`<b>${node.long_name||node.short_name||"Unknown"}</b><br>ID: ${node.node_id}<br>Role: ${node.role}`);
nodeMarkers.set(node.node_id, marker);
} else {
nodeMarkers.set(node.node_id, {options:{nodeInfo:node}});
}
});
const markersWithCoords = Array.from(nodeMarkers.values()).filter(m=>m instanceof L.CircleMarker);
if(markersWithCoords.length>0) await setMapBoundsFromConfig();
else map.setView([37.77,-122.42],9);
} catch(err){
console.error("Failed to load nodes:", err);
}
}
// --- Map bounds ---
async function setMapBoundsFromConfig() {
try {
const res = await fetch("/api/config");
const config = await res.json();
const topLat = parseFloat(config.site.map_top_left_lat);
const topLon = parseFloat(config.site.map_top_left_lon);
const bottomLat = parseFloat(config.site.map_bottom_right_lat);
const bottomLon = parseFloat(config.site.map_bottom_right_lon);
if ([topLat, topLon, bottomLat, bottomLon].some(v => isNaN(v))) {
throw new Error("Map bounds contain NaN");
}
map.fitBounds([[topLat, topLon], [bottomLat, bottomLon]]);
} catch(err) {
console.error("Failed to load map bounds from config:", err);
map.setView([37.77,-122.42],9);
}
}
// --- Poll packets ---
async function pollPackets() {
try {
let url = "/api/packets?limit=10";
if (lastPacketTime) url += `&since=${lastPacketTime}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
const data = await res.json();
const packets = data.packets || [];
if (packets.length) lastPacketTime = packets[0].import_time;
packets.forEach(pkt => {
const marker = nodeMarkers.get(pkt.from_node_id);
if (marker instanceof L.CircleMarker) { // only real markers
marker.currentPortLabel = portLabels[pkt.portnum] || `${pkt.portnum}`;
pulseMarker(marker, getPulseColor(pkt.portnum));
}
});
} catch(err){
console.error("Failed to fetch packets:", err);
}
}
// --- Initialize ---
loadNodes().then(() => setInterval(pollPackets, 1000));
</script>
</body>
</html>
+1 -1
View File
@@ -882,4 +882,4 @@ document.addEventListener("DOMContentLoaded", async () => {
});
});
</script>
{% endblock %}
{% endblock %}
+18 -26
View File
@@ -12,7 +12,7 @@ from google.protobuf import text_format
from google.protobuf.message import Message
from jinja2 import Environment, PackageLoader, Undefined, select_autoescape
from markupsafe import Markup
import pathlib
from meshtastic.protobuf.portnums_pb2 import PortNum
from meshview import config, database, decode_payload, migrations, models, store
from meshview.__version__ import (
@@ -200,6 +200,23 @@ async def redirect_packet_list(request):
packet_id = request.match_info["packet_id"]
raise web.HTTPFound(location=f"/node/{packet_id}")
# Generic static HTML route
@routes.get("/{page}")
async def serve_page(request):
page = request.match_info["page"]
# default to index.html if no extension
if not page.endswith(".html"):
page = f"{page}.html"
html_file = pathlib.Path(__file__).parent / "static" / page
if not html_file.exists():
raise web.HTTPNotFound(text=f"Page '{page}' not found")
content = html_file.read_text(encoding="utf-8")
return web.Response(text=content, content_type="text/html")
@routes.get("/net")
async def net(request):
@@ -394,31 +411,6 @@ async def graph_traceroute(request):
)
'''
@routes.get("/stats")
async def stats(request):
try:
total_packets = await store.get_total_packet_count()
total_nodes = await store.get_total_node_count()
total_packets_seen = await store.get_total_packet_seen_count()
template = env.get_template("stats.html")
return web.Response(
text=template.render(
total_packets=total_packets,
total_nodes=total_nodes,
total_packets_seen=total_packets_seen,
),
content_type="text/html",
)
except Exception as e:
return web.Response(
text=f"An error occurred: {str(e)}",
status=500,
content_type="text/plain",
)
'''
async def run_server():
# Wait for database migrations to complete before starting web server
logger.info("Checking database schema status...")