From 95b371a056be6f0db5d5e2574cf94a61d0d9a471 Mon Sep 17 00:00:00 2001 From: Christian Ditaputratama Date: Wed, 6 Nov 2024 16:45:34 +0700 Subject: [PATCH] feat! Added monero node details page and logs --- internal/handler/response.go | 118 ++-- internal/handler/views/remote_nodes.templ | 123 ++++ internal/handler/views/remote_nodes_templ.go | 657 +++++++++++++++---- internal/monero/report.go | 25 +- internal/monero/report_test.go | 65 +- 5 files changed, 786 insertions(+), 202 deletions(-) diff --git a/internal/handler/response.go b/internal/handler/response.go index 228bb27..57f2b96 100644 --- a/internal/handler/response.go +++ b/internal/handler/response.go @@ -171,45 +171,84 @@ func (s *fiberServer) remoteNodesHandler(c *fiber.Ctx) error { } // Returns a single node information based on `id` query param. -// For now, only process from HTMX request. +// This used for node modal and node details page including node probe logs. func (s *fiberServer) nodeHandler(c *fiber.Ctx) error { + nodeID, err := c.ParamsInt("id", 0) + if err != nil { + return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ + "status": "error", + "message": err.Error(), + "data": nil, + }) + } + if nodeID == 0 { + return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ + "status": "error", + "message": "Invalid node id", + "data": nil, + }) + } + + moneroRepo := monero.New() + node, err := moneroRepo.Node(nodeID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "status": "error", + "message": err.Error(), + "data": nil, + }) + } switch c.Get("HX-Target") { case "modal-section": - nodeID, err := c.ParamsInt("id", 0) - if err != nil { - return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ - "status": "error", - "message": err.Error(), - "data": nil, - }) - } - if nodeID == 0 { - return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ - "status": "error", - "message": "Invalid node id", - "data": nil, - }) - } - moneroRepo := monero.New() - node, err := moneroRepo.Node(nodeID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "status": "error", - "message": err.Error(), - "data": nil, - }) - } cmp := views.ModalLayout(fmt.Sprintf("Node #%d", nodeID), views.Node(node)) handler := adaptor.HTTPHandler(templ.Handler(cmp)) return handler(c) } - // for now, just return 400 - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "status": "error", - "message": "Bad Request, invalid HTMX request", - "data": nil, - }) + queryLogs := monero.QueryLogs{ + Paging: paging.Paging{ + Limit: c.QueryInt("limit", 10), // rows per page + Page: c.QueryInt("page", 1), + SortBy: c.Query("sort_by", "id"), + SortDirection: c.Query("sort_direction", "desc"), + Refresh: c.Query("refresh"), + }, + NodeID: int(node.ID), + Status: c.QueryInt("status", -1), + FailedReason: c.Query("failed_reason"), + } + + logs, err := moneroRepo.Logs(queryLogs) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "status": "error", + "message": err.Error(), + "data": nil, + }) + } + + pagination := paging.NewPagination(queryLogs.Page, logs.TotalPages) + + // handle datatable logs filters, sort request from HTMX + if c.Get("HX-Target") == "tbl_logs" { + cmp := views.BlankLayout(views.TableLogs(fmt.Sprintf("/remote-nodes/id/%d", node.ID), logs, queryLogs, pagination)) + handler := adaptor.HTTPHandler(templ.Handler(cmp)) + return handler(c) + } + + p := views.Meta{ + Title: fmt.Sprintf("%s on Port %d", node.Hostname, node.Port), + Description: fmt.Sprintf("Monero %s remote node %s running on port %d", node.Nettype, node.Hostname, node.Port), + Keywords: fmt.Sprintf("monero log,monero node log,monitoring monero log,monero,xmr,monero node,xmrnode,cryptocurrency,monero %s,%s", node.Nettype, node.Hostname), + Robots: "INDEX,FOLLOW", + Permalink: fmt.Sprintf("https://xmr.ditatompel.com/remote-nodes/id/%d", node.ID), + Identifier: "/remote-nodes", + } + + c.Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, p.Permalink)) + cmp := views.BaseLayout(p, views.NodeDetails(node, logs, queryLogs, pagination)) + handler := adaptor.HTTPHandler(templ.Handler(cmp)) + return handler(c) } // Returns a list of nodes (API) @@ -253,13 +292,16 @@ func Nodes(c *fiber.Ctx) error { func ProbeLogs(c *fiber.Ctx) error { moneroRepo := monero.New() query := monero.QueryLogs{ - RowsPerPage: c.QueryInt("limit", 10), - Page: c.QueryInt("page", 1), - SortBy: c.Query("sort_by", "id"), - SortDirection: c.Query("sort_direction", "desc"), - NodeID: c.QueryInt("node_id", 0), - Status: c.QueryInt("status", -1), - FailedReason: c.Query("failed_reason"), + Paging: paging.Paging{ + Limit: c.QueryInt("limit", 10), // rows per page + Page: c.QueryInt("page", 1), + SortBy: c.Query("sort_by", "id"), + SortDirection: c.Query("sort_direction", "desc"), + Refresh: c.Query("refresh"), + }, + NodeID: c.QueryInt("node_id", 0), + Status: c.QueryInt("status", -1), + FailedReason: c.Query("failed_reason"), } logs, err := moneroRepo.Logs(query) diff --git a/internal/handler/views/remote_nodes.templ b/internal/handler/views/remote_nodes.templ index 7eb57fe..6f99bac 100644 --- a/internal/handler/views/remote_nodes.templ +++ b/internal/handler/views/remote_nodes.templ @@ -210,6 +210,8 @@ templ TableNodes(data monero.Nodes, countries []monero.Countries, q monero.Query { fmt.Sprintf("%d", row.EstimateFee) } @cellUptime(row.Uptime) +
+ [Logs] { timeSince(row.LastChecked) } @@ -228,6 +230,127 @@ templ Node(data monero.Node) {

{ fmt.Sprintf("%s:%d", data.Hostname, data.Port) }

} +templ NodeDetails(data monero.Node, logs monero.FetchLogs, q monero.QueryLogs, p paging.Pagination) { +
+ @heroGradient() +
+
+
+ +
+

{ fmt.Sprintf("%s on port %d", data.Hostname, data.Port) }

+
+
+
+
+ @Node(data) +
+
+
+
+ +
+
+
+

Probe Logs

+
+
+
+ @TableLogs(fmt.Sprintf("/remote-nodes/id/%d", data.ID), logs, q, p) +
+
+} + +templ TableLogs(hxPath string, data monero.FetchLogs, q monero.QueryLogs, p paging.Pagination) { +
+
+ @DtRowPerPage(hxPath, "#tbl_logs", q.Limit, q) +
+ @DtRefreshInterval(hxPath, "#tbl_logs", q.Refresh, q) +
+ @DtReload(hxPath, "#tbl_logs", q) +
+
+ + + + + + + + + + + @DtThSort(hxPath, "#tbl_logs", "Est. Fee", "estimate_fee", q.SortBy, q.SortDirection, q) + @DtThSort(hxPath, "#tbl_logs", "Check", "date_checked", q.SortBy, q.SortDirection, q) + @DtThSort(hxPath, "#tbl_logs", "Runtime", "fetch_runtime", q.SortBy, q.SortDirection, q) + + + + + + + + for _, row := range data.Items { + + + + if row.Status == 1 { + + + + + + + } else { + + + } + + + + } + +
#IDProber IDStatusHeightAdjusted TimeDB SizeDifficulty
+ + + +
{ fmt.Sprintf("%d", row.ID) }{ fmt.Sprintf("%d", row.ProberID) }OK{ fmt.Sprintf("%d", row.Height) }{ time.Unix(row.AdjustedTime, 0).UTC().Format("Jan 2, 2006 15:04 MST") }{ fmt.Sprintf("%d", row.DatabaseSize) }{ fmt.Sprintf("%d", row.Difficulty) }{ fmt.Sprintf("%d", row.EstimateFee) }ERR{ row.FailedReason }{ timeSince(row.DateChecked) }{ formatFloat(row.FetchRuntime) }s
+
+
+ @DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows) + @DtPagination(hxPath, "#tbl_logs", q, p) +
+
+} + templ cellHostPort(id, port uint, hostname, ips string, isTor, ipv6Only bool) { if isTor {