-
+
+
+
+
+
+
+
+
-
@@ -146,7 +246,7 @@
|
- {#each $rows as row}
+ {#each $rows as row (row.id)}
|
-
+
+ |
|
- {format(row.last_checked * 1000, 'PP HH:mm')} |
+
+ {format(row.last_checked * 1000, 'PP HH:mm')}
+ {formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })}
+ |
{/each}
-
diff --git a/frontend/src/routes/(front)/remote-nodes/api-handler.js b/frontend/src/routes/(front)/remote-nodes/api-handler.js
index 5726f3c..138745a 100644
--- a/frontend/src/routes/(front)/remote-nodes/api-handler.js
+++ b/frontend/src/routes/(front)/remote-nodes/api-handler.js
@@ -1,19 +1,18 @@
-import { PUBLIC_API_ENDPOINT } from '$env/static/public';
+import { apiUri } from '$lib/utils/common';
/** @param {import('@vincjo/datatables/remote/state')} state */
-export async function loadApiData(state) {
- const response = await fetch(`${PUBLIC_API_ENDPOINT}/monero/remote-node-dt?${getParams(state)}`);
+export const loadData = async (state) => {
+ const response = await fetch(apiUri(`/api/v1/nodes?${getParams(state)}`));
const json = await response.json();
+ state.setTotalRows(json.data.total_rows ?? 0);
+ return json.data.items ?? [];
+};
- state.setTotalRows(json.data.total ?? 0);
-
- return json.data.nodes ?? [];
-}
-
-const getParams = ({ pageNumber, offset, rowsPerPage, sort, filters }) => {
+const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
+
if (sort) {
- params += `&sort=${sort.orderBy}&dir=${sort.direction}`;
+ params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
}
if (filters) {
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
diff --git a/handler/response.go b/handler/response.go
index 71332c2..87f1502 100644
--- a/handler/response.go
+++ b/handler/response.go
@@ -115,6 +115,32 @@ func Prober(c *fiber.Ctx) error {
})
}
+func MoneroNodes(c *fiber.Ctx) error {
+ moneroRepo := repo.NewMoneroRepo(database.GetDB())
+ query := repo.MoneroQueryParams{
+ RowsPerPage: c.QueryInt("limit", 10),
+ Page: c.QueryInt("page", 1),
+ SortBy: c.Query("sort_by", "id"),
+ SortDirection: c.Query("sort_direction", "desc"),
+ Host: c.Query("host"),
+ }
+
+ nodes, err := moneroRepo.Nodes(query)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "status": "error",
+ "message": err.Error(),
+ "data": nil,
+ })
+ }
+
+ return c.JSON(fiber.Map{
+ "status": "ok",
+ "message": "Success",
+ "data": nodes,
+ })
+}
+
func AddNode(c *fiber.Ctx) error {
formPort := c.FormValue("port")
port, err := strconv.Atoi(formPort)
diff --git a/handler/routes.go b/handler/routes.go
index cae95e6..235f666 100644
--- a/handler/routes.go
+++ b/handler/routes.go
@@ -14,6 +14,7 @@ func V1Api(app *fiber.App) {
v1.Get("/prober", Prober)
v1.Post("/prober", Prober)
+ v1.Get("/nodes", MoneroNodes)
v1.Post("/nodes", AddNode)
v1.Get("/crons", Crons)
}
diff --git a/internal/repo/monero.go b/internal/repo/monero.go
index 6eb416a..57c431c 100644
--- a/internal/repo/monero.go
+++ b/internal/repo/monero.go
@@ -3,15 +3,20 @@ package repo
import (
"encoding/json"
"errors"
+ "fmt"
"net"
+ "slices"
"strings"
"time"
"github.com/ditatompel/xmr-nodes/internal/database"
+
+ "github.com/jmoiron/sqlx/types"
)
type MoneroRepository interface {
Add(protocol string, host string, port uint) error
+ Nodes(q MoneroQueryParams) (MoneroNodes, error)
}
type MoneroRepo struct {
@@ -22,6 +27,111 @@ func NewMoneroRepo(db *database.DB) MoneroRepository {
return &MoneroRepo{db}
}
+type MoneroNode struct {
+ Id uint `json:"id,omitempty" db:"id"`
+ Hostname string `json:"hostname" db:"hostname"`
+ Ip string `json:"ip" db:"ip_addr"`
+ Port uint `json:"port" db:"port"`
+ Protocol string `json:"protocol" db:"protocol"`
+ IsTor bool `json:"is_tor" db:"is_tor"`
+ IsAvailable bool `json:"is_available" db:"is_available"`
+ NetType string `json:"nettype" db:"nettype"`
+ LastHeight uint `json:"last_height" db:"last_height"`
+ AdjustedTime uint `json:"adjusted_time" db:"adjusted_time"`
+ DatabaseSize uint `json:"database_size" db:"database_size"`
+ Difficulty uint `json:"difficulty" db:"difficulty"`
+ NodeVersion string `json:"node_version" db:"node_version"`
+ Uptime float32 `json:"uptime" db:"uptime"`
+ EstimateFee uint `json:"estimate_fee" db:"estimate_fee"`
+ Asn uint `json:"asn" db:"asn"`
+ AsnName string `json:"asn_name" db:"asn_name"`
+ CountryCode string `json:"cc" db:"country"`
+ CountryName string `json:"country_name" db:"country_name"`
+ City string `json:"city" db:"city"`
+ Lat float64 `json:"latitude" db:"lat"`
+ Lon float64 `json:"longitude" db:"lon"`
+ DateEntered uint `json:"date_entered,omitempty" db:"date_entered"`
+ LastChecked uint `json:"last_checked" db:"last_checked"`
+ FailedCount uint `json:"failed_count,omitempty" db:"failed_count"`
+ LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"`
+ CorsCapable bool `json:"cors" db:"cors_capable"`
+}
+
+type MoneroNodes struct {
+ TotalRows int `json:"total_rows"`
+ RowsPerPage int `json:"rows_per_page"`
+ CurrentPage int `json:"current_page"`
+ NextPage int `json:"next_page"`
+ Items []*MoneroNode `json:"items"`
+}
+
+type MoneroQueryParams struct {
+ Host string
+ RowsPerPage int
+ Page int
+ SortBy string
+ SortDirection string
+}
+
+func (repo *MoneroRepo) Nodes(q MoneroQueryParams) (MoneroNodes, error) {
+ queryParams := []interface{}{}
+ whereQueries := []string{}
+ where := ""
+
+ if q.Host != "" {
+ whereQueries = append(whereQueries, "(hostname LIKE ? OR ip_addr LIKE ?)")
+ queryParams = append(queryParams, "%"+q.Host+"%")
+ queryParams = append(queryParams, "%"+q.Host+"%")
+ }
+
+ if len(whereQueries) > 0 {
+ where = "WHERE " + strings.Join(whereQueries, " AND ")
+ }
+
+ nodes := MoneroNodes{}
+
+ queryTotalRows := fmt.Sprintf("SELECT COUNT(id) AS total_rows FROM tbl_node %s", where)
+
+ err := repo.db.QueryRow(queryTotalRows, queryParams...).Scan(&nodes.TotalRows)
+ if err != nil {
+ return nodes, err
+ }
+ queryParams = append(queryParams, q.RowsPerPage, (q.Page-1)*q.RowsPerPage)
+
+ allowedSort := []string{"last_checked", "uptime"}
+ sortBy := "last_checked"
+ if slices.Contains(allowedSort, q.SortBy) {
+ sortBy = q.SortBy
+ }
+ sortDirection := "DESC"
+ if q.SortDirection == "asc" {
+ sortDirection = "ASC"
+ }
+
+ query := fmt.Sprintf("SELECT id, protocol, hostname, port, is_tor, is_available, nettype, last_height, adjusted_time, database_size, difficulty, node_version, uptime, estimate_fee, ip_addr, asn, asn_name, country, country_name, city, lat, lon, date_entered, last_checked, last_check_status, cors_capable FROM tbl_node %s ORDER BY %s %s LIMIT ? OFFSET ?", where, sortBy, sortDirection)
+
+ row, err := repo.db.Query(query, queryParams...)
+ if err != nil {
+ return nodes, err
+ }
+ defer row.Close()
+
+ nodes.RowsPerPage = q.RowsPerPage
+ nodes.CurrentPage = q.Page
+ nodes.NextPage = q.Page + 1
+
+ for row.Next() {
+ node := MoneroNode{}
+ err = row.Scan(&node.Id, &node.Protocol, &node.Hostname, &node.Port, &node.IsTor, &node.IsAvailable, &node.NetType, &node.LastHeight, &node.AdjustedTime, &node.DatabaseSize, &node.Difficulty, &node.NodeVersion, &node.Uptime, &node.EstimateFee, &node.Ip, &node.Asn, &node.AsnName, &node.CountryName, &node.CountryCode, &node.City, &node.Lat, &node.Lon, &node.DateEntered, &node.LastChecked, &node.LastCheckStatus, &node.CorsCapable)
+ if err != nil {
+ return nodes, err
+ }
+ nodes.Items = append(nodes.Items, &node)
+ }
+
+ return nodes, nil
+}
+
func (repo *MoneroRepo) Add(protocol string, hostname string, port uint) error {
if protocol != "http" && protocol != "https" {
return errors.New("Invalid protocol, must one of or HTTP/HTTPS")
diff --git a/tools/resources/database/structure.sql b/tools/resources/database/structure.sql
index f88b2fb..8913e7b 100644
--- a/tools/resources/database/structure.sql
+++ b/tools/resources/database/structure.sql
@@ -39,6 +39,39 @@ CREATE TABLE `tbl_cron` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tbl_node`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `tbl_node` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `protocol` varchar(6) NOT NULL DEFAULT 'http' COMMENT 'http | https',
+ `hostname` varchar(200) NOT NULL DEFAULT '',
+ `port` int(6) unsigned NOT NULL DEFAULT 0,
+ `is_tor` tinyint(1) unsigned NOT NULL DEFAULT 0,
+ `is_available` tinyint(1) unsigned NOT NULL DEFAULT 0,
+ `nettype` varchar(100) NOT NULL COMMENT 'mainnet | stagenet | testnet',
+ `last_height` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `adjusted_time` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `database_size` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `difficulty` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `node_version` varchar(200) NOT NULL DEFAULT '',
+ `uptime` float(5,2) unsigned NOT NULL DEFAULT 0.00,
+ `estimate_fee` int(9) unsigned NOT NULL DEFAULT 0,
+ `ip_addr` varchar(200) NOT NULL,
+ `asn` int(9) unsigned NOT NULL DEFAULT 0,
+ `asn_name` varchar(200) NOT NULL DEFAULT '',
+ `country` varchar(200) NOT NULL DEFAULT '',
+ `country_name` varchar(255) NOT NULL DEFAULT '',
+ `city` varchar(200) NOT NULL DEFAULT '',
+ `lat` float NOT NULL DEFAULT 0 COMMENT 'latitude',
+ `lon` float NOT NULL DEFAULT 0 COMMENT 'longitude',
+ `date_entered` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `last_checked` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `last_check_status` text DEFAULT NULL,
+ `cors_capable` tinyint(1) unsigned NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tbl_prober`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;