mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2025-01-05 18:39:30 +00:00
603 lines
22 KiB
Text
603 lines
22 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"
|
|
)
|
|
|
|
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 and I2P addresses, click the <span class="text-orange-300">👁 hostname...</span> to open more detailed information about the Node.</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.IsI2P, row.IPv6Only)
|
|
</td>
|
|
<td>
|
|
@fmtNettype(row.Nettype)
|
|
<br/>
|
|
{ fmt.Sprintf("%d", row.Height) }
|
|
</td>
|
|
<td>
|
|
@fmtProtocol(row.Protocol)
|
|
if row.CORSCapable {
|
|
<br/>
|
|
(CORS 💪)
|
|
}
|
|
</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">
|
|
@fmtProtocol(data.Protocol)
|
|
if data.CORSCapable {
|
|
<span class="ml-2">(CORS 💪)</span>
|
|
}
|
|
</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">
|
|
if data.IsI2P {
|
|
<span class="badge bg-green-600 mr-2">I2P</span>
|
|
} else if data.IsTor {
|
|
<span class="badge bg-purple-800 mr-2">TOR</span>
|
|
}
|
|
@fmtNettype(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>
|
|
}
|
|
if data.CountryCode != "" {
|
|
<dl class="flex flex-col sm:flex-row gap-1">
|
|
<dt class="min-w-40">
|
|
<span class="block text-white text-bold">Country:</span>
|
|
</dt>
|
|
<dd>
|
|
<ul>
|
|
<li class="whitespace-break-spaces">
|
|
@cellCountry(data.CountryCode, data.CountryName, data.City, data.ASNName, data.ASN)
|
|
</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">Monitored Since:</span>
|
|
</dt>
|
|
<dd>
|
|
<ul>
|
|
<li class="whitespace-break-spaces">
|
|
{ time.Unix(data.DateEntered, 0).UTC().Format("Jan 2, 2006 15:04 MST") } (about { utils.TimeSince(data.DateEntered) })
|
|
</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">cURL get_info Eg.:</span>
|
|
</dt>
|
|
<dd>
|
|
<ul>
|
|
<li>
|
|
<label for="curl-getinfo-eg" class="sr-only">cURL get_info Example</label>
|
|
<div class="flex rounded-lg shadow-sm">
|
|
<input type="text" id="curl-getinfo-eg" name="stagenet-ssl" class="py-1 px-2 block w-full text-neutral-400 bg-neutral-900 border-neutral-700 shadow-sm rounded-0 text-sm focus:z-10 focus:border-orange-500 focus:ring-orange-500" value={ monero.ParseCURLGetInfo(data) } readonly/>
|
|
<button class="clipboard copy-input" data-clipboard-target="#curl-getinfo-eg">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
</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 border-orange-400"/>
|
|
</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 fmtNettype(nettype string) {
|
|
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>
|
|
}
|
|
}
|
|
|
|
templ fmtProtocol(protocol string) {
|
|
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>
|
|
}
|
|
}
|
|
|
|
templ cellHostPort(id, port uint, hostname, ips string, isTor, isI2P, 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="badge bg-purple-800">TOR</span>
|
|
} else if isI2P {
|
|
<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/>
|
|
.i2p:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
|
|
<span class="badge bg-green-600">I2P</span>
|
|
} else {
|
|
<button
|
|
class="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"
|
|
>
|
|
👁 { ip.FormatHostname(hostname) }
|
|
</button>
|
|
:<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 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>
|
|
}
|
|
}
|