mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2025-01-08 05:52:10 +07:00
Monero remote node UI for frontend
This commit also implement the simple remote node queries. TODO: Add filter for various data
This commit is contained in:
parent
7cd802e640
commit
ca759fc1d0
6 changed files with 297 additions and 24 deletions
|
@ -1,5 +1,82 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData } from './api-handler';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination
|
||||
} from '$lib/components/datatables/server';
|
||||
import {
|
||||
HostPortCell,
|
||||
NetTypeCell,
|
||||
ProtocolCell,
|
||||
CountryCellWithAsn,
|
||||
StatusCell,
|
||||
UptimeCell,
|
||||
EstimateFeeCell
|
||||
} from './components';
|
||||
|
||||
export let data;
|
||||
let filterNettype = 'any';
|
||||
let filterProtocol = 'any';
|
||||
let filterCc = 'any';
|
||||
let filterStatus = -1;
|
||||
let checkboxCors = false;
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
const reloadData = () => {
|
||||
handler.invalidate();
|
||||
};
|
||||
|
||||
/** @type {number | undefined} */
|
||||
let intervalId;
|
||||
let intervalValue = 0;
|
||||
|
||||
const intervalOptions = [
|
||||
{ value: 0, label: 'No' },
|
||||
{ value: 5, label: '5s' },
|
||||
{ value: 10, label: '10s' },
|
||||
{ value: 30, label: '30s' },
|
||||
{ value: 60, label: '1m' }
|
||||
];
|
||||
|
||||
const startInterval = () => {
|
||||
const seconds = intervalValue;
|
||||
if (isNaN(seconds) || seconds < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!intervalOptions.some((option) => option.value === seconds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
reloadData();
|
||||
intervalId = setInterval(() => {
|
||||
reloadData();
|
||||
}, seconds * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
$: startInterval(); // Automatically start the interval on change
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalId); // Clear the interval when the component is destroyed
|
||||
});
|
||||
onMount(() => {
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
|
@ -36,19 +113,40 @@
|
|||
<div class="section-container">
|
||||
<div class="space-y-2 overflow-x-auto">
|
||||
<div class="flex justify-between">
|
||||
<!-- <DtSrRowsPerPage {handler} /> -->
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<label for="autoRefreshInterval">Auto Refresh:</label>
|
||||
<select
|
||||
class="select ml-2"
|
||||
id="autoRefreshInterval"
|
||||
bind:value={intervalValue}
|
||||
on:change={startInterval}
|
||||
>
|
||||
{#each intervalOptions as { value, label }}
|
||||
<option {value}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={reloadData}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host:Port</th>
|
||||
<th><label for="fNettype">Nettype</label></th>
|
||||
<th><label for="fProtocol">Protocol</label></th>
|
||||
<th><label for="fCc">Country</label></th>
|
||||
<th><label for="fStatus">Status</label></th>
|
||||
<th>Nettype</th>
|
||||
<th>Protocol</th>
|
||||
<th>Country</th>
|
||||
<th>Status</th>
|
||||
<th>Est. Fee</th>
|
||||
|
||||
<DtSrThSort {handler} orderBy="uptime">Uptime</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="last_checked">Check</DtSrThSort>
|
||||
</tr>
|
||||
|
@ -100,6 +198,7 @@
|
|||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
<!--
|
||||
{#each data.countries as country}
|
||||
{#if country.cc === ''}
|
||||
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
|
||||
|
@ -109,6 +208,7 @@
|
|||
>
|
||||
{/if}
|
||||
{/each}
|
||||
-->
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
|
@ -146,7 +246,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row}
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td
|
||||
><HostPortCell
|
||||
|
@ -173,23 +273,27 @@
|
|||
statuses={row.last_check_statuses}
|
||||
/></td
|
||||
>
|
||||
<td
|
||||
><EstimateFeeCell
|
||||
<td>
|
||||
<!-- <EstimateFeeCell
|
||||
estimate_fee={row.estimate_fee}
|
||||
majority_fee={netFees[row.nettype]}
|
||||
/></td
|
||||
>
|
||||
/>
|
||||
-->
|
||||
</td>
|
||||
<td><UptimeCell uptime={row.uptime} /></td>
|
||||
<td>{format(row.last_checked * 1000, 'PP HH:mm')}</td>
|
||||
<td>
|
||||
{format(row.last_checked * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex justify-between">
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 */;
|
||||
|
|
Loading…
Reference in a new issue