xmr-remote-nodes/internal/handler/views/remote_nodes.templ
Christian Ditaputratama 98dcdfa94a
feat: Do not push query strings to URL #155
Slightly increase user browsing privacy by not pushing query strings
to browser URL. By using this method, the browser history stay on the
main page and filter query strings not recorded.

Note: This approach is experimental. Only tested on Firefox and Chromium
 browser.
2024-11-07 01:20:50 +07:00

533 lines
20 KiB
Text

package views
import (
"fmt"
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
"github.com/ditatompel/xmr-remote-nodes/internal/paging"
"github.com/ditatompel/xmr-remote-nodes/utils"
"strings"
"time"
)
var nettypes = []string{"mainnet", "stagenet", "testnet"}
var protocols = []string{"tor", "http", "https"}
type nodeStatus struct {
Code int
Text string
}
var nodeStatuses = []nodeStatus{
{-1, "ANY"},
{1, "Online"},
{0, "Offline"},
}
templ RemoteNodes(data monero.Nodes, countries []monero.Countries, q monero.QueryNodes, p paging.Pagination) {
<!-- Hero -->
<section class="relative overflow-hidden pt-6">
@heroGradient()
<div class="relative z-10">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16">
<div class="text-center">
<!-- Title -->
<div class="mt-5">
<h1 class="block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200">Public Monero Remote Nodes List</h1>
</div>
<!-- End Title -->
<div class="mt-5">
<p class="text-lg text-neutral-300"><strong>Monero remote node</strong> is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.</p>
</div>
</div>
</div>
</div>
</section>
<!-- End Hero -->
<section class="flex flex-col max-w-6xl mx-auto mb-10">
<div class="min-w-full inline-block align-middle">
@TableNodes(data, countries, q, p)
</div>
</section>
<section id="page-info" class="max-w-4xl mx-auto px-4 mb-10">
<div class="p-4 bg-blue-800/10 border border-blue-900 text-sm text-white rounded-lg" role="alert" tabindex="-1" aria-labelledby="add-node-notice">
<div class="flex">
<div class="ms-4">
<h2 id="add-node-notice" class="text-xl font-bold text-center">Info</h2>
<div class="mt-2 text-sm">
<ul class="list-disc space-y-1 ps-5">
<li>If you find any remote nodes that are strange or suspicious, please <a href="https://github.com/ditatompel/xmr-remote-nodes/issues" target="_blank" rel="noopener" class="external">open an issue on GitHub</a> for removal.</li>
<li>Uptime percentage calculated is the <strong>last 1 month</strong> uptime.</li>
<li><strong>Est. Fee</strong> here is just fee estimation / byte from <code class="code text-green-500 font-bold">get_fee_estimate</code> RPC call method.</li>
<li>Malicious actors who running remote nodes <a href="/assets/img/node-tx-fee.jpg" rel="noopener" class="link" hx-boost="false">still can return high fee only if you about to create a transactions</a>.</li>
<li><strong class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500">The best and safest way is running your own node!</strong></li>
<li>Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You can always add your node again latter.</li>
<li>You can filter remote node by selecting on <strong>nettype</strong>, <strong>protocol</strong>, <strong>country</strong>, <strong>tor</strong>, and <strong>online status</strong> option.</li>
<li>If you want to add more remote node, you can add them using <a href="/add-node" class="link">/add-node</a> page.</li>
<li>I deliberately cut the long Tor addresses, click the <span class="text-orange-300">👁 torhostname...</span> to see the full Tor address.</li>
<li>You can found larger remote nodes database from <a href="https://monero.fail/" target="_blank" rel="noopener" class="external">monero.fail</a>.</li>
<li>If you are developer or power user who like to fetch Monero remote node above in JSON format, you can read <a href="https://insights.ditatompel.com/en/blog/2022/01/public-api-monero-remote-node-list/" class="external">Public API Monero Remote Node List</a> blog post for more detailed information.</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<div class="max-w-4xl text-center mx-auto my-10 prose prose-invert">
<p>Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.</p>
<p>Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the <strong>Monero community suggests to <span class="font-extrabold underline decoration-double decoration-2 decoration-pink-500">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
</div>
}
templ TableNodes(data monero.Nodes, countries []monero.Countries, q monero.QueryNodes, p paging.Pagination) {
<div id="tbl_nodes" class="bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700">
@DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q)
<div>
@DtRefreshInterval("/remote-nodes", "#tbl_nodes", q.Refresh, q)
</div>
@DtReload("/remote-nodes", "#tbl_nodes", q)
</div>
<div class="overflow-x-auto">
<table class="dt">
<thead>
<tr>
<th scope="col">Host:Port</th>
<th scope="col">Nettype</th>
<th scope="col">Protocol</th>
<th scope="col">Country</th>
<th scope="col">Status</th>
<th scope="col">Estimate Fee</th>
@DtThSort("/remote-nodes", "#tbl_nodes", "Uptime", "uptime", q.SortBy, q.SortDirection, q)
@DtThSort("/remote-nodes", "#tbl_nodes", "Check", "last_checked", q.SortBy, q.SortDirection, q)
</tr>
<tr>
<td>
<input
type="text"
id="host"
name="host"
value={ fmt.Sprintf("%s", q.Host) }
autocomplete="off"
class="frameless"
placeholder="Filter Host / IP"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"host"})) }
hx-push-url="false"
hx-trigger="keyup changed delay:0.4s"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
/>
</td>
<td>
<select
id="nettype"
name="nettype"
class="frameless"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"nettype"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="">ANY</option>
for _, nettype := range nettypes {
<option value={ fmt.Sprintf("%s", nettype) } selected?={ nettype == q.Nettype }>{ nettype }</option>
}
</select>
</td>
<td>
<select
id="protocol"
name="protocol"
class="frameless"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"protocol"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="">ANY</option>
for _, protocol := range protocols {
<option value={ fmt.Sprintf("%s", protocol) } selected?={ protocol == q.Protocol }>{ protocol }</option>
}
</select>
</td>
<td>
<select
id="cc"
name="cc"
class="frameless"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cc"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="any">ANY</option>
for _, country := range countries {
if country.CC == "" {
<option value="UNKNOWN" selected?={ q.CC== "UNKNOWN" }>{ fmt.Sprintf("UNKNOWN (%d)", country.TotalNodes ) }</option>
} else {
<option value={ fmt.Sprintf("%s", country.CC) } selected?={ country.CC == q.CC }>{ fmt.Sprintf("%s (%d)", country.Name, country.TotalNodes ) }</option>
}
}
</select>
</td>
<td colspan="2">
<select
id="status"
name="status"
class="frameless"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"status"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
for _, status := range nodeStatuses {
<option value={ fmt.Sprintf("%d", status.Code) } selected?={ status.Code == q.Status }>{ status.Text }</option>
}
</select>
</td>
<td colspan="2">
<div class="flex justify-center">
<input
type="checkbox"
id="cors"
name="cors"
autocomplete="off"
checked?={ q.CORS == "on" }
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cors"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
class="shrink-0 mt-0.5 text-orange-400 bg-neutral-800 border-neutral-700 rounded focus:ring-0 checked:bg-orange-400 checked:border-orange-400 focus:ring-offset-orange-500"
/>
<label for="cors" class="text-sm ms-3 text-neutral-400">CORS</label>
</div>
</td>
</tr>
</thead>
<tbody>
for _, row := range data.Items {
<tr>
<td>
@cellHostPort(row.ID, row.Port, row.Hostname, row.IPAddresses, row.IsTor, row.IPv6Only)
</td>
<td>
@cellNettype(row.Nettype, row.Height)
</td>
<td>
@cellProtocol(row.Protocol, row.CORSCapable)
</td>
<td>
@cellCountry(row.CountryCode, row.CountryName, row.City, row.ASNName, row.ASN)
</td>
<td>
@cellStatuses(row.IsAvailable, monero.ParseNodeStatuses(row.LastCheckStatus))
</td>
<td class="text-right">{ fmt.Sprintf("%d", row.EstimateFee) }</td>
<td class="text-right">
@cellUptime(row.Uptime)
<br/>
<a href={ templ.URL(fmt.Sprintf("/remote-nodes/id/%d", row.ID)) } class="link">[Logs]</a>
</td>
<td title={ time.Unix(row.LastChecked, 0).UTC().Format("Jan 2, 2006 15:04 MST") }>{ utils.TimeSince(row.LastChecked) }</td>
</tr>
}
</tbody>
</table>
</div>
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700">
@DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows)
@DtPagination("/remote-nodes", "#tbl_nodes", q, p)
</div>
</div>
}
templ Node(data monero.Node) {
<div class="space-y-3 text-neutral-200">
<dl class="flex flex-col sm:flex-row gap-1">
<dt class="min-w-40">
<span class="block text-white text-bold">Host:</span>
</dt>
<dd>
<ul>
<li class="me-1 inline-flex items-center">
{ fmt.Sprintf("%s:%d", data.Hostname, data.Port) }
</li>
<li class="me-1 inline-flex items-center">
<button type="button" class="clipboard px-2 inline-flex items-center gap-x-2 text-sm font-bold rounded-lg border border-transparent bg-orange-600 text-white hover:bg-orange-500 focus:outline-none disabled:opacity-60 disabled:pointer-events-none" data-clipboard-text={ fmt.Sprintf("%s:%d", data.Hostname, data.Port) }>Copy</button>
</li>
</ul>
</dd>
</dl>
<dl class="flex flex-col sm:flex-row gap-1">
<dt class="min-w-40">
<span class="block text-white text-bold">Protocol:</span>
</dt>
<dd>
<ul>
<li class="uppercase">{ data.Protocol }</li>
</ul>
</dd>
</dl>
if data.Nettype != "" {
<dl class="flex flex-col sm:flex-row gap-1">
<dt class="min-w-40">
<span class="block text-white text-bold">Net Type:</span>
</dt>
<dd>
<ul>
<li class="uppercase">{ data.Nettype }</li>
</ul>
</dd>
</dl>
}
if data.IPAddresses != "" {
<dl class="flex flex-col sm:flex-row gap-1">
<dt class="min-w-40">
<span class="block text-white text-bold">IP Addresses:</span>
</dt>
<dd>
<ul>
<li class="whitespace-break-spaces">{ strings.ReplaceAll(data.IPAddresses, ",", ", ") }</li>
</ul>
</dd>
</dl>
}
</div>
}
templ NodeDetails(data monero.Node, logs monero.FetchLogs, q monero.QueryLogs, p paging.Pagination) {
<section class="relative overflow-hidden pt-6">
@heroGradient()
<div class="relative z-10">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16">
<div class="text-center">
<!-- Title -->
<div class="mt-5">
<h1 class="block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200">
Monero Node #{ fmt.Sprintf("%d", data.ID) }
</h1>
</div>
<hr class="mt-6"/>
</div>
<div class="max-w-3xl mx-auto mt-8">
@Node(data)
</div>
</div>
</div>
</section>
<!-- End Hero -->
<div class="flex flex-col max-w-6xl mx-auto mb-10">
<div class="my-6 text-center">
<div class="mt-5">
<h2 class="block font-extrabold text-4xl md:text-4xl lg:text-5xl text-neutral-200">Probe Logs</h2>
</div>
</div>
<div class="min-w-full inline-block align-middle">
@TableLogs(fmt.Sprintf("/remote-nodes/id/%d", data.ID), logs, q, p)
</div>
</div>
}
templ TableLogs(hxPath string, data monero.FetchLogs, q monero.QueryLogs, p paging.Pagination) {
<div id="tbl_logs" class="bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700">
@DtRowPerPage(hxPath, "#tbl_logs", q.Limit, q)
<div>
@DtRefreshInterval(hxPath, "#tbl_logs", q.Refresh, q)
</div>
@DtReload(hxPath, "#tbl_logs", q)
</div>
<div class="overflow-x-auto">
<table class="dt">
<thead>
<tr>
<th scope="col">#ID</th>
<th scope="col">Prober ID</th>
<th scope="col">Status</th>
<th scope="col">Height</th>
<th scope="col">Adjusted Time</th>
<th scope="col">DB Size</th>
<th scope="col">Difficulty</th>
@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)
</tr>
<tr>
<td colspan="3">
<select
id="status"
name="status"
class="frameless"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", hxPath, paging.EncodedQuery(q, []string{"status"})) }
hx-trigger="change"
hx-push-url="false"
hx-target="#tbl_logs"
hx-swap="outerHTML"
>
for _, status := range nodeStatuses {
<option value={ fmt.Sprintf("%d", status.Code) } selected?={ status.Code == q.Status }>{ status.Text }</option>
}
</select>
</td>
<td colspan="7">
<input
type="text"
id="failed_reason"
name="failed_reason"
value={ fmt.Sprintf("%s", q.FailedReason) }
autocomplete="off"
class="frameless"
placeholder="Filter reason"
hx-get={ fmt.Sprintf("%s?%s", hxPath, paging.EncodedQuery(q, []string{"failed_reason"})) }
hx-push-url="false"
hx-trigger="keyup changed delay:0.4s"
hx-target="#tbl_logs"
hx-swap="outerHTML"
/>
</td>
</tr>
</thead>
<tbody>
for _, row := range data.Items {
<tr>
<td>{ fmt.Sprintf("%d", row.ID) }</td>
<td>{ fmt.Sprintf("%d", row.ProberID) }</td>
if row.Status == 1 {
<td class="text-green-500">OK</td>
<td class="text-right">{ fmt.Sprintf("%d", row.Height) }</td>
<td>{ time.Unix(row.AdjustedTime, 0).UTC().Format("Jan 2, 2006 15:04 MST") }</td>
<td>{ utils.FormatBytes(row.DatabaseSize, 0) }</td>
<td>{ utils.FormatHashes(float64(row.Difficulty)) }</td>
<td class="text-right">{ fmt.Sprintf("%d", row.EstimateFee) }</td>
} else {
<td class="text-red-500">ERR</td>
<td colspan="5">{ row.FailedReason }</td>
}
<td title={ time.Unix(row.DateChecked, 0).UTC().Format("Jan 2, 2006 15:04 MST") }>{ utils.TimeSince(row.DateChecked) }</td>
<td class="text-right">{ utils.FormatFloat(row.FetchRuntime) }s</td>
</tr>
}
</tbody>
</table>
</div>
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700">
@DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows)
@DtPagination(hxPath, "#tbl_logs", q, p)
</div>
</div>
}
templ cellHostPort(id, port uint, hostname, ips string, isTor, ipv6Only bool) {
if isTor {
<button
class="max-w-40 truncate text-orange-400 hover:brightness-125"
hx-get={ fmt.Sprintf("/remote-nodes/id/%d", id) }
hx-push-url="false"
hx-target="#modal-section"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="modal-section"
data-hs-overlay="#modal-section"
>
👁 { hostname }
</button>
<br/>
.onion:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
<span class="text-neutral-400">(TOR)</span>
} else {
{ ip.FormatHostname(hostname) }:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
<br/>
<div class="max-w-40 text-ellipsis overflow-x-auto md:overflow-hidden hover:overflow-visible">
<span class="whitespace-break-spaces text-gray-400">{ strings.ReplaceAll(ips, ",", " ") }</span>
if ipv6Only {
<span class="text-rose-400">(IPv6 only)</span>
}
</div>
}
}
templ cellNettype(nettype string, height uint) {
switch nettype {
case "stagenet":
<span class="font-semibold uppercase text-sky-500">{ nettype }</span>
case "testnet":
<span class="font-semibold uppercase text-rose-500">{ nettype }</span>
default:
<span class="font-semibold uppercase text-green-500">{ nettype }</span>
}
<br/>
{ fmt.Sprintf("%d", height) }
}
templ cellProtocol(protocol string, cors bool) {
switch protocol {
case "http":
<span class="font-semibold uppercase text-sky-500">{ protocol }</span>
default:
<span class="font-semibold uppercase text-green-500">{ protocol }</span>
}
if cors {
<br/>
(CORS 💪)
}
}
templ cellCountry(cc, countryName, city, asnName string, asn uint) {
if cc != "" {
if city != "" {
{ city },
}
{ countryName }
<img class="inline-block" src={ fmt.Sprintf("/assets/img/cf/%s.svg", strings.ToLower(cc)) } alt={ fmt.Sprintf("%s Flag", cc) } width="22px"/>
}
if asn != 0 {
<br/>
<a
class="external font-semibold underline !text-purple-400"
href={ templ.URL(fmt.Sprintf("https://www.ditatompel.com/asn/%d", asn)) }
target="_blank"
rel="noopener"
>{ fmt.Sprintf("AS%d", asn) }</a>
(<span class="font-semibold text-green-500">{ asnName }</span>)
}
}
templ cellStatuses(isAvailable bool, statuses [5]int) {
if isAvailable {
<span class="font-semibold text-green-500">Online</span>
} else {
<span class="text-rose-400">Offline</span>
}
<br/>
for _, status := range statuses {
if status == 1 {
<span class="text-green-400 mr-1">•</span>
} else if status == 0 {
<span class="text-red-400 mr-1">•</span>
} else {
<span class="text-neutral-600 mr-1">•</span>
}
}
}
templ cellUptime(uptime float64) {
if uptime >= 98 {
<span class="text-green-500">{ utils.FormatFloat(uptime) }%</span>
} else if uptime < 98 && uptime >= 80 {
<span class="text-sky-500">{ utils.FormatFloat(uptime) }%</span>
} else if uptime < 80 && uptime > 75 {
<span class="text-orange-500">{ utils.FormatFloat(uptime) }%</span>
} else {
<span class="text-rose-500">{ utils.FormatFloat(uptime) }%</span>
}
}