diff --git a/meshview/templates/nodelist.html b/meshview/templates/nodelist.html index 98b50be..8132204 100644 --- a/meshview/templates/nodelist.html +++ b/meshview/templates/nodelist.html @@ -9,7 +9,6 @@ table { margin-right: auto; } - th, td { padding: 10px; border: 1px solid #333; @@ -67,7 +66,32 @@ tr:nth-child(odd) { width: 80%; margin-left: auto; margin-right: auto; +} +.pagination { + display: flex; + justify-content: center; + margin-top: 1em; + gap: 6px; +} + +.pagination li { + list-style: none; +} + +.pagination li a { + color: white; + background-color: #333; + padding: 6px 12px; + text-decoration: none; + border-radius: 4px; + border: 1px solid #555; + font-size: 14px; +} + +.pagination li.active a { + background-color: #007bff; + border-color: #007bff; } {% endblock %} @@ -139,6 +163,9 @@ tr:nth-child(odd) { {% endfor %} + + +
{% else %}No nodes found.
{% endif %} @@ -153,14 +180,19 @@ tr:nth-child(odd) { valueNames: [ "long_name", "short_name", "hw_model", "firmware", "role", "last_lat", "last_long", "channel", { name: "last_update", attr: "data-timestamp" } - ] + ], + page: 50, + pagination: { + paginationClass: "pagination" + } }; + nodeList = new List("node-list", options); - updateCount(); // Update count on load + updateCount(); nodeList.on("updated", function () { - updateCount(); // Update count when search or sort changes + updateCount(); }); }); @@ -176,7 +208,7 @@ tr:nth-child(odd) { return matchesRole && matchesChannel && matchesHWModel; }); - updateCount(); // Update the count after filtering + updateCount(); } function updateCount() { @@ -189,22 +221,21 @@ tr:nth-child(odd) { var rows = table.querySelectorAll("tr"); var csvContent = []; - // Extract header row var headers = []; table.querySelectorAll("th").forEach(th => headers.push(th.innerText)); csvContent.push(headers.join(",")); - // Extract table rows rows.forEach(row => { - var cells = row.querySelectorAll("td"); - if (cells.length > 0) { - var rowData = []; - cells.forEach(cell => rowData.push(cell.innerText)); - csvContent.push(rowData.join(",")); + if (row.style.display !== "none") { + var cells = row.querySelectorAll("td"); + if (cells.length > 0) { + var rowData = []; + cells.forEach(cell => rowData.push(cell.innerText)); + csvContent.push(rowData.join(",")); + } } }); - // Create CSV file and trigger download var csvString = csvContent.join("\n"); var blob = new Blob([csvString], { type: "text/csv" }); var a = document.createElement("a"); diff --git a/meshview/web.py b/meshview/web.py index c82046a..c395647 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -1020,27 +1020,36 @@ async def nodelist(request): content_type="text/plain", ) - @routes.get("/api") async def api(request): try: + # Extract optional query parameters role = request.query.get("role") channel = request.query.get("channel") hw_model = request.query.get("hw_model") + # Fetch filtered nodes nodes = await store.get_nodes(role, channel, hw_model) + # Convert node objects to dictionaries for JSON output nodes_json = [node.to_dict() for node in nodes] - return web.json_response({"nodes": nodes_json}) + + # Return a pretty-printed JSON response + return web.json_response( + {"nodes": nodes_json}, + dumps=lambda obj: json.dumps(obj, indent=2) # Pretty print for development + ) except Exception as e: - import traceback + # Log error and stack trace to console print("Error in /api endpoint:", str(e)) print(traceback.format_exc()) + + # Return a plain-text error response return web.Response( text=f"An error occurred: {str(e)}", status=500, - content_type="text/plain", + content_type="text/plain" )