Compare commits
237 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e8efd2fc02 | ||
|
dc27b693d8 | ||
|
d7991acb03 | ||
|
c7167b118e | ||
|
0ac3d6837b | ||
|
9fe1ed5351 | ||
|
8e32103ae4 | ||
|
31edbc0ecf | ||
|
866c48e85f | ||
|
7e66528180 | ||
|
2bc41ed1a8 | ||
|
6b4f880dbd | ||
|
6bee3768f6 | ||
|
44ac10bb8a | ||
|
60a4c83011 | ||
|
e66e087d44 | ||
|
ff682dd0bc | ||
|
8a3921edde | ||
|
bdd85078cf | ||
|
16681f6b51 | ||
|
46a29fb9cc | ||
|
48a25bece0 | ||
|
d19e5844b0 | ||
|
f1b9ffde33 | ||
|
b780783df0 | ||
|
84ad053413 | ||
|
9b48c4731a | ||
|
a936cb343b | ||
|
76c6a5514d | ||
|
56b445d0aa | ||
|
ecfee86b2c | ||
|
ce02334669 | ||
|
8c48e4749b | ||
|
eb6d709bbb | ||
|
114078d3a1 | ||
|
8e79d20b29 | ||
|
64da0beff9 | ||
|
5e2ab83295 | ||
|
e0313bdbe2 | ||
|
f339bc9c3c | ||
|
e892733a55 | ||
|
15804ee438 | ||
|
060b3a3827 | ||
|
2e361a9fab | ||
|
98dcdfa94a | ||
|
c1c72274cf | ||
|
5a22a0b71f | ||
|
d60dbd86be | ||
|
e66f5bb1b8 | ||
|
b68f626ce2 | ||
|
2e31824910 | ||
|
5fb88865d0 | ||
|
f227371fa6 | ||
|
3f5c2b9905 | ||
|
df161f831a | ||
|
75e97b4e0c | ||
|
9e1da3c79a | ||
|
0f011572f5 | ||
|
3beb3ba60e | ||
|
fb6f6c2b5c | ||
|
1eb26210f6 | ||
|
95b371a056 | ||
|
fdf541f78f | ||
|
a8c94ca0aa | ||
|
0acf12a277 | ||
|
7da5fdb10c | ||
|
1cd1b1a9c6 | ||
|
e5eb23997b | ||
|
335f87b6d5 | ||
|
721d1e8d6b | ||
|
9cebe9d12f | ||
|
6e7eccc6b3 | ||
|
f0a10208e2 | ||
|
c3b6f587ed | ||
|
efc86d66fd | ||
|
0165f0c251 | ||
|
e524c2686d | ||
|
204865e50d | ||
|
97f6312ce9 | ||
|
ea0e0df57d | ||
|
c3c18ced05 | ||
|
44722f6b43 | ||
|
babe61258a | ||
|
7b5287fe9a | ||
|
f2cc795dc2 | ||
|
b23b0ae31a | ||
|
751bfbc585 | ||
|
6efa763e73 | ||
|
10182d9dbc | ||
|
ca3ca881fd | ||
|
ec6f0a1893 | ||
|
30aa8d80dc | ||
|
f6adb40b3f | ||
|
93fb22f29b | ||
|
4da9c484a5 | ||
|
63e803ba17 | ||
|
ddc837be4a | ||
|
4dfab11d2c | ||
|
0a80a52d2d | ||
|
176a02412a | ||
|
8b39502d90 | ||
|
965d3230a1 | ||
|
dd48bd458a | ||
|
2003c3c3ac | ||
|
ca0af5848c | ||
|
3a45071cd6 | ||
|
35d53c8a58 | ||
|
be32011cfa | ||
|
03570a2200 | ||
|
68c4b7c9b3 | ||
|
a963963b72 | ||
|
7cc2b1035f | ||
|
dd0095a767 | ||
|
b340849d9d | ||
|
f4b42911c6 | ||
|
ddab7555b3 | ||
|
37d5031b2a | ||
|
7d91c8d3b2 | ||
|
bec557df09 | ||
|
3153c291e0 | ||
|
70330eccdc | ||
|
c398816317 | ||
|
6e99c9ef16 | ||
|
2a0d273aac | ||
|
4066b78dab | ||
|
2bba91de73 | ||
|
a9a8c92f9f | ||
|
ae2257f45b | ||
|
004f0a604f | ||
|
b85f70f395 | ||
|
df66a8ad59 | ||
|
ae7f1842ae | ||
|
001cc0fee8 | ||
|
9054537b15 | ||
|
7004256160 | ||
|
40dac1f70e | ||
|
25bc4d5439 | ||
|
348c4543ee | ||
|
fc3cb7a913 | ||
|
6040a59350 | ||
|
f6b7963cf3 | ||
|
e8915265a6 | ||
|
91b4845bb5 | ||
|
598fc3f13d | ||
|
490ac692f2 | ||
|
114cc9e4e1 | ||
|
4d3a022e68 | ||
|
d0affd12f5 | ||
|
283503f5d9 | ||
|
77f4d434c7 | ||
|
fbb48eb827 | ||
|
a158af7e22 | ||
|
b8ce89f7e5 | ||
|
c09b63cf84 | ||
|
ed83e917b7 | ||
|
ce89742066 | ||
|
b038d32627 | ||
|
6382c02c4c | ||
|
fa8579e4f7 | ||
|
97f2256ba8 | ||
|
d527e305c5 | ||
|
f4f5e34889 | ||
|
26cabb7a94 | ||
|
7dc7ec4d9e | ||
|
5362eb7474 | ||
|
9c7e1f2091 | ||
|
c838f166d1 | ||
|
d3a2c90c92 | ||
|
3da4e61de1 | ||
|
ce6eac4c0e | ||
|
3f6920ee65 | ||
|
5f2bcdf01e | ||
|
ef953b36fd | ||
|
76a54a0f29 | ||
|
a9bf9473d6 | ||
|
664738ca82 | ||
|
bc97e6249f | ||
|
6de247870b | ||
|
c94945cd22 | ||
|
846b8bd6fb | ||
|
dcf8b6cfc6 | ||
|
89ed02dc96 | ||
|
85da44da46 | ||
|
273fcef8c7 | ||
|
83b5c20cf4 | ||
|
d78eea3da0 | ||
|
1c67b09229 | ||
|
89b90775ee | ||
|
5119ee75b1 | ||
|
71c8737cff | ||
|
b60a67c8cb | ||
|
9bd609e4dd | ||
|
cdb0816bc3 | ||
|
fc172a0bd0 | ||
|
7553ad8b45 | ||
|
f6b048b017 | ||
|
0e3dc04af8 | ||
|
61cc98e378 | ||
|
c3f837e122 | ||
|
d698b0d9af | ||
|
a7bf9470fb | ||
|
3b9212618b | ||
|
6365b8a3c8 | ||
|
518d4b4335 | ||
|
187a7bc84a | ||
|
79bc58b87f | ||
|
71b44eabac | ||
|
bd7ddab4f3 | ||
|
62773a9d3b | ||
|
a3ce4ee75e | ||
|
06f183efda | ||
|
e09fa18d6e | ||
|
c7878eabee | ||
|
169785402e | ||
|
a02b109508 | ||
|
654e02c424 | ||
|
24dfea165a | ||
|
50b6e43352 | ||
|
fb482c9e20 | ||
|
90c16c7c25 | ||
|
893724d7e9 | ||
|
3fad791ca5 | ||
|
df435a0411 | ||
|
e5d8b4e4d9 | ||
|
33306c0603 | ||
|
5bd8301ec5 | ||
|
19e4f69348 | ||
|
59c00f716b | ||
|
7a82840572 | ||
|
debea78e45 | ||
|
d71b19ef7b | ||
|
ec1d12f902 | ||
|
32705f3005 | ||
|
50f0aab1e0 | ||
|
64ab2aa185 | ||
|
9345e75b9c | ||
|
46c209d2b9 |
|
@ -7,14 +7,14 @@ tmp_dir = "tmp"
|
|||
bin = "./tmp/main"
|
||||
cmd = "make dev"
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "testdata", "frontend/node_modules", "data", "bin"]
|
||||
exclude_dir = ["assets", "tmp", "testdata", "node_modules", "data", "bin", "internal/handler/views/assets"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_regex = ["_test.go", ".*_templ.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html", "css", "js"]
|
||||
include_ext = ["go", "templ", "html", "css", "js"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
|
|
|
@ -8,9 +8,18 @@ SERVER_ENDPOINT="http://127.0.0.1:18901"
|
|||
API_KEY=
|
||||
ACCEPT_TOR=false
|
||||
TOR_SOCKS="127.0.0.1:9050"
|
||||
ACCEPT_I2P=false
|
||||
I2P_SOCKS="127.0.0.1:4447"
|
||||
IPV6_CAPABLE=false
|
||||
|
||||
# Server Config
|
||||
# #############
|
||||
APP_URL="https://xmr.ditatompel.com" # URL where user can access the web UI, don't put trailing slash
|
||||
|
||||
# APP_SECRET is random 64-character hex string that give us 32 random bytes.
|
||||
# For now, this used for ip address salt, but may be useful for another feature
|
||||
# in the future. You can achieve this using `openssl rand -hex 32`.
|
||||
APP_SECRET=
|
||||
|
||||
# Fiber Config
|
||||
APP_PREFORK=false
|
||||
|
|
11
.github/dependabot.yml
vendored
|
@ -10,14 +10,3 @@ updates:
|
|||
- ditatompel
|
||||
assignees:
|
||||
- ditatompel
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "05:00"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- ditatompel
|
||||
assignees:
|
||||
- ditatompel
|
||||
|
|
23
.github/workflows/build.yml
vendored
|
@ -14,24 +14,19 @@ jobs:
|
|||
- name: Check out source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: setup NodeJS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.23.x
|
||||
|
||||
- name: Setup templ
|
||||
run: go install github.com/a-h/templ/cmd/templ@v0.3.857
|
||||
|
||||
- name: Prepare assets
|
||||
run: make prepare
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v3
|
||||
|
|
16
.github/workflows/release.yml
vendored
|
@ -16,20 +16,20 @@ jobs:
|
|||
- name: Check out source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: setup NodeJS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.23.x
|
||||
|
||||
- name: Setup templ
|
||||
run: go install github.com/a-h/templ/cmd/templ@v0.3.857
|
||||
|
||||
# Need to build the UI here before build the server binary with go-release-action
|
||||
- name: Build UI
|
||||
run: make ui
|
||||
- name: Prepare assets
|
||||
run: make prepare templ tailwind
|
||||
|
||||
- name: Build server binary
|
||||
uses: wangyoucao577/go-release-action@v1
|
||||
|
|
31
.github/workflows/test.yml
vendored
|
@ -2,6 +2,9 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- htmx
|
||||
- i2p-support
|
||||
|
||||
pull_request:
|
||||
name: Test
|
||||
jobs:
|
||||
|
@ -12,10 +15,16 @@ jobs:
|
|||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.23.x
|
||||
|
||||
- name: Setup templ
|
||||
run: go install github.com/a-h/templ/cmd/templ@v0.3.857
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v3
|
||||
|
@ -25,27 +34,13 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Prepare assets
|
||||
run: make prepare templ tailwind
|
||||
|
||||
- name: Run lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: setup NodeJS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Build UI
|
||||
run: make ui
|
||||
|
||||
- name: Run test
|
||||
run: make test
|
||||
|
|
2
.gitignore
vendored
|
@ -3,3 +3,5 @@
|
|||
/node_modules
|
||||
/tmp
|
||||
/assets/geoip
|
||||
/internal/handler/views/assets/css/**/*
|
||||
/internal/handler/views/assets/js/**/*
|
||||
|
|
43
LICENSE
|
@ -1,27 +1,28 @@
|
|||
GLWTS(Good Luck With That Shit) Public License
|
||||
Copyright (c) Every-fucking-one, except the Author
|
||||
Copyright (c) 2024, Christian Ditaputratama
|
||||
|
||||
Everyone is permitted to copy, distribute, modify, merge, sell, publish,
|
||||
sublicense or whatever the fuck they want with this software but at their
|
||||
OWN RISK.
|
||||
All rights reserved.
|
||||
|
||||
Preamble
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
The author has absolutely no fucking clue what the code in this project
|
||||
does. It might just fucking work or not, there is no third option.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
GOOD LUCK WITH THAT SHIT PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION
|
||||
|
||||
0. You just DO WHATEVER THE FUCK YOU WANT TO as long as you NEVER LEAVE
|
||||
A FUCKING TRACE TO TRACK THE AUTHOR of the original product to blame for
|
||||
or hold responsible.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Good luck and Godspeed.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
42
Makefile
|
@ -31,16 +31,12 @@ BUILD_LDFLAGS := -s -w -X github.com/ditatompel/xmr-remote-nodes/internal/config
|
|||
|
||||
# This called from air cmd (see .air.toml)
|
||||
.PHONY: dev
|
||||
dev:
|
||||
dev: templ tailwind
|
||||
go build -ldflags="$(BUILD_LDFLAGS)" -tags server -o ./tmp/main .
|
||||
|
||||
.PHONY: build
|
||||
build: client server
|
||||
|
||||
.PHONY: ui
|
||||
ui:
|
||||
go generate ./...
|
||||
|
||||
.PHONY: client
|
||||
client:
|
||||
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build \
|
||||
|
@ -51,7 +47,7 @@ client:
|
|||
-o bin/${BINARY_NAME}-client-linux-arm64
|
||||
|
||||
.PHONY: server
|
||||
server: ui
|
||||
server: prepare templ tailwind
|
||||
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build \
|
||||
-ldflags="$(BUILD_LDFLAGS)" -tags server \
|
||||
-o bin/${BINARY_NAME}-server-linux-amd64
|
||||
|
@ -59,11 +55,37 @@ server: ui
|
|||
-ldflags="$(BUILD_LDFLAGS)" -tags server \
|
||||
-o bin/${BINARY_NAME}-server-linux-arm64
|
||||
|
||||
.PHONY: prepare
|
||||
prepare:
|
||||
bun install --frozen-lockfile
|
||||
@mkdir -p ./internal/handler/views/assets/js
|
||||
cp ./node_modules/htmx.org/dist/htmx.min.js ./internal/handler/views/assets/js
|
||||
cp ./node_modules/clipboard/dist/clipboard.min.js ./internal/handler/views/assets/js
|
||||
|
||||
# Compile template
|
||||
.PHONY: templ
|
||||
templ:
|
||||
@echo "Compiling Templ template..."
|
||||
templ generate
|
||||
|
||||
.PHONY: tailwind
|
||||
tailwind:
|
||||
mkdir -p ./internal/handler/views/assets/css
|
||||
@echo "Compiling TailwindCSS..."
|
||||
bun tailwindcss -i ./internal/handler/views/src/css/main.css \
|
||||
-o ./internal/handler/views/assets/css/main.min.css \
|
||||
-c ./tailwind.config.js \
|
||||
--minify
|
||||
bun build ./internal/handler/views/src/js/main.js --minify \
|
||||
--outfile ./internal/handler/views/assets/js/main.min.js
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
go clean
|
||||
rm -rfv ./bin
|
||||
rm -rf ./frontend/build
|
||||
rm -rfv ./tmp/main
|
||||
rm -rf ./internal/handler/views/*_templ.go
|
||||
rm -rf ./internal/handler/views/assets/css/
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
|
@ -82,8 +104,10 @@ bench:
|
|||
# And make sure the inventory and deploy-*.yml file is properly configured.
|
||||
.PHONY: deploy-server
|
||||
deploy-server:
|
||||
ansible-playbook -i ./deployment/ansible/inventory.ini -l server ./deployment/ansible/deploy-server.yml -K
|
||||
ansible-playbook -i ./deployment/ansible/inventory.ini \
|
||||
-l server ./deployment/ansible/deploy-server.yml -K
|
||||
|
||||
.PHONY: deploy-prober
|
||||
deploy-prober:
|
||||
ansible-playbook -i ./deployment/ansible/inventory.ini -l prober ./deployment/ansible/deploy-prober.yml -K
|
||||
ansible-playbook -i ./deployment/ansible/inventory.ini \
|
||||
-l prober ./deployment/ansible/deploy-prober.yml -K
|
||||
|
|
100
README.md
|
@ -5,65 +5,129 @@
|
|||
[](https://github.com/ditatompel/xmr-remote-nodes/actions/workflows/release.yml)
|
||||
[](https://goreportcard.com/report/github.com/ditatompel/xmr-remote-nodes)
|
||||
|
||||
Source code of [https://xmr.ditatompel.com](https://xmr.ditatompel.com), a website that helps you monitor your favourite Monero remote nodes.
|
||||
Source code of [https://xmr.ditatompel.com](https://xmr.ditatompel.com),
|
||||
a website that helps you monitor your favourite Monero remote nodes.
|
||||
|
||||
> :warning: :construction: This project is still freaking 0.x.x :construction:,
|
||||
> :warning: :construction: This project is not mature enough :construction:,
|
||||
> If you want to use it on your server, please use it with caution.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Apart from CPU architecture type, you can build two types of binaries from this project: a **server** and a **client**.
|
||||
Apart from CPU architecture type, you can build two types of binaries from
|
||||
this project: a **server** and a **client**.
|
||||
|
||||
The **clients** is used to fetch node information given by the server. First, it will ask the server which node to fetch. Then, it will fetch the information and report back to the server.
|
||||
The **clients** is used to fetch node information given by the server. First,
|
||||
it will ask the server which node to fetch. Then, it will fetch the information
|
||||
and report back to the server.
|
||||
|
||||
The **server** serves an embedded Svelte static site for the Web UI. It also serves the `/api` endpoint that is used by the clients and the Web UI itself.
|
||||
The **server** serves the Web UI and the `/api` endpoint that is used by the
|
||||
clients.
|
||||
|
||||
## Requirements
|
||||
|
||||
To build the executable binaries, you need:
|
||||
|
||||
- Go >= 1.22
|
||||
- NodeJS >= 20
|
||||
- Go >= 1.23
|
||||
- Bun >= 1.1.26
|
||||
- [a-h/templ][templ-repo] v0.3.857
|
||||
|
||||
> **Note**:
|
||||
>
|
||||
> - If you want to contribute to the code, please use exact templ version
|
||||
> (v0.3.857).
|
||||
|
||||
### Server & Prober requirements
|
||||
|
||||
- Linux Machines (AMD64 or ARM64)
|
||||
- Linux Machines (AMD64 or ARM64)
|
||||
|
||||
### Server requirements
|
||||
|
||||
- MySQL/MariaDB
|
||||
- [GeoIP Database](https://dev.maxmind.com/geoip/geoip2/geolite2/) (optional). Place it to `./assets/geoip`, see [./internal/geo/ip.go](./internal/geo/ip.go).
|
||||
- MySQL/MariaDB
|
||||
- [GeoIP Database][geoip-doc] (optional). Place it to `./assets/geoip`,
|
||||
see [./internal/ip/geo/geoip.go](./internal/ip/geo/geoip.go).
|
||||
|
||||
## Installation
|
||||
|
||||
### For initial server setup:
|
||||
|
||||
1. Download [GeoIP Database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and place it to `./assets/geoip`. (see [./internal/geo/ip.go](./internal/geo/ip.go)).
|
||||
1. Download [GeoIP Database][geoip-doc] and place it to `./assets/geoip`.
|
||||
(see [./internal/ip/geo/geoip.go](./internal/ip/geo/geoip.go)).
|
||||
2. Pepare your MySQL/MariaDB.
|
||||
3. Copy `.env.example` to `.env` and edit it to match with server environment.
|
||||
4. Build the binary with `make server` (or `make build` to build both **server** and **client** binaries).
|
||||
4. Build the binary with `make server` (or `make build` to build both
|
||||
**server** and **client** binaries).
|
||||
5. Run the service with `./bin/xmr-nodes-server-linux-<YOUR_CPU_ARCH> serve`.
|
||||
|
||||
Systemd example: [./deployment/init/xmr-nodes-server.service](./deployment/init/xmr-nodes-server.service).
|
||||
Systemd example: [xmr-nodes-server.service][server-systemd-service].
|
||||
|
||||
### For initial prober setup:
|
||||
|
||||
1. Create API key for prober
|
||||
2. Copy `.env.example` to `.env` and edit it to match with prober environment.
|
||||
3. Build the binary with `make client` (or `make build` to build both **server** and **client** binaries).
|
||||
3. Build the binary with `make client` (or `make build` to build both
|
||||
**server** and **client** binaries).
|
||||
4. Run the service with `./bin/xmr-nodes-client-linux-<YOUR_CPU_ARCH> probe`.
|
||||
|
||||
Systemd example: [xmr-nodes-prober.service](./deployment/init/xmr-nodes-prober.service) and [xmr-nodes-prober.timer](./deployment/init/xmr-nodes-prober.timer).
|
||||
Systemd example: [xmr-nodes-prober.service][prober-systemd-service] and
|
||||
[xmr-nodes-prober.timer][prober-systemd-timer].
|
||||
|
||||
## Development and Deployment
|
||||
|
||||
1. Clone or fork this repository.
|
||||
2. Prepare the assets: `make prepare`,
|
||||
3. Run `air serve` (live reload using [air-verse/air][air-repo]).
|
||||
|
||||
See the [Makefile](./Makefile).
|
||||
|
||||
## ToDo's
|
||||
|
||||
- :white_check_mark: Accept IPv6 nodes.
|
||||
- :white_check_mark: Use `a-h/templ` and `HTMX` instead of `Svelte`.
|
||||
- Use Go standard `net/http` instead of `fiber`.
|
||||
- :white_check_mark: Accept I2P nodes.
|
||||
- :white_check_mark: Support Tor hidden service (beta, inform via HTTP header).
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
The creators and contributors of these projects have provided valuable
|
||||
resources, which I am grateful for:
|
||||
|
||||
- [jtgrassie/monero-pool][jtgrassie-monero-pool]
|
||||
- [rclone/rclone][rclone]
|
||||
|
||||
## Similar Projects
|
||||
|
||||
- [lalanza808/monero.fail](https://github.com/lalanza808/monero.fail)
|
||||
- [cake-tech/upptime-monerocom](https://github.com/cake-tech/upptime-monerocom)
|
||||
- [lalanza808/monero.fail][monerofail-repo]
|
||||
- [cake-tech/upptime-monerocom][uptime-monerocom-repo]
|
||||
|
||||
## Donation
|
||||
|
||||
The servers costs are currently covered by myself. If you find this project
|
||||
useful, please consider making a donation to help cover the ongoing expenses.
|
||||
Your contribution will go towards ensuring the continued availability of the
|
||||
website and **my** `stagenet` and `testnet` public remote nodes.
|
||||
|
||||
XMR Donation address:
|
||||
|
||||
```plain
|
||||
8BWYe6GzbNKbxe3D8mPkfFMQA2rViaZJFhWShhZTjJCNG6EZHkXRZCKHiuKmwwe4DXDYF8KKcbGkvNYaiRG3sNt7JhnVp7D
|
||||
```
|
||||
|
||||

|
||||
|
||||
Thank you!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under [GLWTPL](./LICENSE).
|
||||
This project is licensed under [BSD-3-Clause](./LICENSE) license.
|
||||
|
||||
[templ-repo]: https://github.com/a-h/templ "a-h/templ GitHub repository"
|
||||
[geoip-doc]: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/ "GeoLite2 Free documentation"
|
||||
[server-systemd-service]: ./deployment/init/xmr-nodes-server.service "systemd service example for server"
|
||||
[prober-systemd-service]: ./deployment/init/xmr-nodes-prober.service "systemd service example for prober"
|
||||
[prober-systemd-timer]: ./deployment/init/xmr-nodes-prober.timer "systemd timer example for prober"
|
||||
[air-repo]: https://github.com/air-verse/air "Air - Live reload for Go apps"
|
||||
[jtgrassie-monero-pool]: https://github.com/jtgrassie/monero-pool "A Monero mining pool server written in C"
|
||||
[rclone]: https://github.com/rclone/rclone "rclone GitHub repository"
|
||||
[monerofail-repo]: https://github.com/lalanza808/monero.fail "Lalanza808's monero.fail GitHub repository"
|
||||
[uptime-monerocom-repo]: https://github.com/cake-tech/upptime-monerocom "monero.com uptime GitHub repository"
|
||||
|
|
2
VERSION
|
@ -1 +1 @@
|
|||
v0.0.5
|
||||
v0.2.2
|
||||
|
|
BIN
bun.lockb
Executable file
|
@ -14,17 +14,19 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/config"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
const RPCUserAgent = "ditatombot/0.0.1 (Monero RPC Monitoring; https://github.com/ditatompel/xmr-remote-nodes)"
|
||||
const RPCUserAgent = "ditatombot/0.0.2 (Monero RPC Monitoring; https://github.com/ditatompel/xmr-remote-nodes)"
|
||||
|
||||
const (
|
||||
errNoEndpoint = errProber("no SERVER_ENDPOINT was provided")
|
||||
errNoTorSocks = errProber("no TOR_SOCKS was provided")
|
||||
errNoI2PSocks = errProber("no I2P_SOCKS was provided")
|
||||
errNoAPIKey = errProber("no API_KEY was provided")
|
||||
errInvalidCredentials = errProber("invalid API_KEY credentials")
|
||||
)
|
||||
|
@ -36,20 +38,26 @@ func (err errProber) Error() string {
|
|||
}
|
||||
|
||||
type proberClient struct {
|
||||
endpoint string // server endpoint
|
||||
apiKey string // prober api key
|
||||
acceptTor bool // accept tor
|
||||
torSOCKS string // IP:Port of tor socks
|
||||
message string // message to include when reporting back to server
|
||||
endpoint string // server endpoint
|
||||
apiKey string // prober api key
|
||||
acceptTor bool // accept tor
|
||||
torSOCKS string // IP:Port of tor socks
|
||||
acceptI2P bool // accept i2p
|
||||
I2PSOCKS string // IP:Port of i2p socks
|
||||
acceptIPv6 bool // accept ipv6
|
||||
message string // message to include when reporting back to server
|
||||
}
|
||||
|
||||
func newProber() *proberClient {
|
||||
cfg := config.AppCfg()
|
||||
return &proberClient{
|
||||
endpoint: cfg.ServerEndpoint,
|
||||
apiKey: cfg.APIKey,
|
||||
acceptTor: cfg.AcceptTor,
|
||||
torSOCKS: cfg.TorSOCKS,
|
||||
endpoint: cfg.ServerEndpoint,
|
||||
apiKey: cfg.APIKey,
|
||||
acceptTor: cfg.AcceptTor,
|
||||
torSOCKS: cfg.TorSOCKS,
|
||||
acceptI2P: cfg.AcceptI2P,
|
||||
I2PSOCKS: cfg.I2PSOCKS,
|
||||
acceptIPv6: cfg.IPv6Capable,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +72,9 @@ var ProbeCmd = &cobra.Command{
|
|||
if t, _ := cmd.Flags().GetBool("no-tor"); t {
|
||||
prober.SetAcceptTor(false)
|
||||
}
|
||||
if t, _ := cmd.Flags().GetBool("no-i2p"); t {
|
||||
prober.SetAcceptI2P(false)
|
||||
}
|
||||
|
||||
if err := prober.Run(); err != nil {
|
||||
switch err.(type) {
|
||||
|
@ -85,6 +96,14 @@ func (p *proberClient) SetAcceptTor(acceptTor bool) {
|
|||
p.acceptTor = acceptTor
|
||||
}
|
||||
|
||||
func (p *proberClient) SetAcceptI2P(acceptI2P bool) {
|
||||
p.acceptI2P = acceptI2P
|
||||
}
|
||||
|
||||
func (p *proberClient) SetAcceptIPv6(acceptIPv6 bool) {
|
||||
p.acceptIPv6 = acceptIPv6
|
||||
}
|
||||
|
||||
// Fetch a new job from the server, fetches node info, and sends it to the server
|
||||
func (p *proberClient) Run() error {
|
||||
if err := p.validateConfig(); err != nil {
|
||||
|
@ -115,26 +134,40 @@ func (p *proberClient) validateConfig() error {
|
|||
if p.acceptTor && p.torSOCKS == "" {
|
||||
return errNoTorSocks
|
||||
}
|
||||
if p.acceptI2P && p.I2PSOCKS == "" {
|
||||
return errNoI2PSocks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get monero node info to fetch from the server
|
||||
func (p *proberClient) fetchJob() (monero.Node, error) {
|
||||
queryParams := ""
|
||||
acceptTor := 0
|
||||
if p.acceptTor {
|
||||
queryParams = "?accept_tor=1"
|
||||
acceptTor = 1
|
||||
}
|
||||
|
||||
acceptI2P := 0
|
||||
if p.acceptI2P {
|
||||
acceptI2P = 1
|
||||
}
|
||||
|
||||
acceptIPv6 := 0
|
||||
if p.acceptIPv6 {
|
||||
acceptIPv6 = 1
|
||||
}
|
||||
|
||||
var node monero.Node
|
||||
|
||||
uri := fmt.Sprintf("%s/api/v1/job%s", p.endpoint, queryParams)
|
||||
uri := fmt.Sprintf("%s/api/v1/job?accept_tor=%d&accept_i2p=%d&accept_ipv6=%d", p.endpoint, acceptTor, acceptI2P, acceptIPv6)
|
||||
slog.Info(fmt.Sprintf("[PROBE] Getting node from %s", uri))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
||||
req.Header.Add(monero.ProberAPIKey, p.apiKey)
|
||||
req.Header.Set("User-Agent", RPCUserAgent)
|
||||
|
||||
|
@ -185,8 +218,16 @@ func (p *proberClient) fetchNode(node monero.Node) (monero.Node, error) {
|
|||
req.Header.Set("Origin", "https://xmr.ditatompel.com")
|
||||
|
||||
var client http.Client
|
||||
var socks5 string
|
||||
|
||||
if p.acceptTor && node.IsTor {
|
||||
dialer, err := proxy.SOCKS5("tcp", p.torSOCKS, nil, proxy.Direct)
|
||||
socks5 = p.torSOCKS
|
||||
} else if p.acceptI2P && node.IsI2P {
|
||||
socks5 = p.I2PSOCKS
|
||||
}
|
||||
|
||||
if socks5 != "" {
|
||||
dialer, err := proxy.SOCKS5("tcp", socks5, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
@ -255,7 +296,7 @@ func (p *proberClient) fetchNode(node monero.Node) (monero.Node, error) {
|
|||
node.CORSCapable = true
|
||||
}
|
||||
|
||||
if !node.IsTor {
|
||||
if !node.IsTor && !node.IsI2P {
|
||||
hostIp, err := net.LookupIP(node.Hostname)
|
||||
if err != nil {
|
||||
fmt.Println("Warning: Could not resolve hostname: " + node.Hostname)
|
||||
|
@ -322,6 +363,13 @@ func (p *proberClient) fetchFee(client http.Client, endpoint string) (uint, erro
|
|||
}
|
||||
|
||||
func (p *proberClient) reportResult(node monero.Node, tookTime float64) error {
|
||||
if !node.IsTor && !node.IsI2P {
|
||||
if hostIps, err := net.LookupIP(node.Hostname); err == nil {
|
||||
node.IPv6Only = ip.IsIPv6Only(hostIps)
|
||||
node.IPAddresses = ip.SliceToString(hostIps)
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(monero.ProbeReport{
|
||||
TookTime: tookTime,
|
||||
Message: p.message,
|
||||
|
|
|
@ -29,7 +29,8 @@ func init() {
|
|||
Root.PersistentFlags().StringVarP(&configFile, "config-file", "c", "", "Default to .env")
|
||||
Root.AddCommand(client.ProbeCmd)
|
||||
client.ProbeCmd.Flags().StringP("endpoint", "e", "", "Server endpoint")
|
||||
client.ProbeCmd.Flags().Bool("no-tor", false, "Only probe clearnet nodes")
|
||||
client.ProbeCmd.Flags().Bool("no-tor", false, "Do not probe tor nodes")
|
||||
client.ProbeCmd.Flags().Bool("no-i2p", false, "Do not probe i2p nodes")
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
|
|
|
@ -12,4 +12,6 @@ func init() {
|
|||
probersCmd.AddCommand(deleteProbersCmd)
|
||||
listProbersCmd.Flags().StringP("sort-by", "s", "last_submit_ts", "Sort by column name, can be id or last_submit_ts")
|
||||
listProbersCmd.Flags().StringP("sort-dir", "d", "desc", "Sort direction, can be asc or desc")
|
||||
cmd.Root.AddCommand(nodeCmd)
|
||||
nodeCmd.AddCommand(deleteNodeCmd)
|
||||
}
|
||||
|
|
59
cmd/server/node.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/database"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var nodeCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "[Server] Administer monitored nodes",
|
||||
Long: `Command to administer monitored nodes.
|
||||
|
||||
This command should only be run on the server which directly connect to the MySQL database.
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
if err := cmd.Help(); err != nil {
|
||||
slog.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var deleteNodeCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete node",
|
||||
Long: `Delete node identified by ID.
|
||||
|
||||
This command delete node and it's associated probe logs (if exists).
|
||||
|
||||
To find out the node ID, visit frontend UI or from "/api/v1/nodes" endpoint.
|
||||
`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
if err := database.ConnectDB(); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
nodeID, err := strconv.Atoi(stringPrompt("Node ID:"))
|
||||
if err != nil {
|
||||
fmt.Println("Invalid ID:", err)
|
||||
return
|
||||
}
|
||||
|
||||
moneroRepo := monero.New()
|
||||
err = moneroRepo.Delete(uint(nodeID))
|
||||
if err != nil {
|
||||
fmt.Println("Failed to delete node:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Node ID %d deleted\n", nodeID)
|
||||
},
|
||||
}
|
|
@ -8,15 +8,14 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ditatompel/xmr-remote-nodes/frontend"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/config"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/cron"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/database"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/handler"
|
||||
"github.com/ditatompel/xmr-remote-nodes/internal/handler/views"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -56,11 +55,7 @@ func serve() {
|
|||
}
|
||||
|
||||
// Define Fiber config & app.
|
||||
app := fiber.New(fiber.Config{
|
||||
Prefork: appCfg.Prefork,
|
||||
ProxyHeader: appCfg.ProxyHeader,
|
||||
AppName: "XMR Nodes Aggregator",
|
||||
})
|
||||
app := handler.NewServer()
|
||||
|
||||
// recover
|
||||
app.Use(recover.New(recover.Config{EnableStackTrace: true}))
|
||||
|
@ -79,11 +74,8 @@ func serve() {
|
|||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
handler.V1Api(app)
|
||||
app.Use("/", filesystem.New(filesystem.Config{
|
||||
Root: frontend.SvelteKitHandler(),
|
||||
// NotFoundFile: "index.html",
|
||||
}))
|
||||
app.Use("/assets", views.EmbedAssets())
|
||||
app.Routes()
|
||||
|
||||
// go routine to capture system calls
|
||||
go func() {
|
||||
|
|
|
@ -44,6 +44,9 @@ server {
|
|||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Download-Options noopen;
|
||||
|
||||
# Add your onion URL here if you support it
|
||||
# add_header Onion-Location http://<YOUR-ONION-ADDRESS>.onion$request_uri;
|
||||
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
|
|
10
frontend/.gitignore
vendored
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
|
@ -1,4 +0,0 @@
|
|||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# UI
|
||||
|
||||
The UI is generated and embedded when the Go project is built. See [./frontend/embed.go](https://github.com/ditatompel/xmr-remote-nodes/blob/main/frontend/embed.go#L10-L13).
|
|
@ -1,21 +0,0 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate npm ci
|
||||
//go:generate npm run build
|
||||
//go:embed build/*
|
||||
var f embed.FS
|
||||
|
||||
func SvelteKitHandler() http.FileSystem {
|
||||
build, err := fs.Sub(f, "build")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return http.FS(build)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
/** @type {import('eslint').Linter.FlatConfig[]} */
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
];
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
4094
frontend/package-lock.json
generated
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"name": "xmr-nodes-frontend",
|
||||
"version": "v0.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "VITE_API_URL=http://127.0.0.1:18901 vite dev --host 127.0.0.1",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
"@skeletonlabs/skeleton": "^2.10.2",
|
||||
"@skeletonlabs/tw-plugin": "^0.4.0",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@vincjo/datatables": "^1.14.10",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"date-fns": "^3.3.1",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.5",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full;
|
||||
}
|
||||
p {
|
||||
@apply mb-2;
|
||||
}
|
||||
.link {
|
||||
@apply text-primary-800 dark:text-primary-500 hover:brightness-110;
|
||||
}
|
||||
a.external {
|
||||
@apply link after:content-['_↗'];
|
||||
}
|
||||
.section-container {
|
||||
@apply mx-auto w-full max-w-7xl p-4;
|
||||
}
|
||||
.hero-gradient {
|
||||
background-image: radial-gradient(at 0% 0%, rgba(242, 104, 34, 0.4) 0px, transparent 50%),
|
||||
radial-gradient(at 98% 1%, rgba(var(--color-warning-900) / 0.33) 0px, transparent 50%);
|
||||
}
|
33
frontend/src/app.d.ts
vendored
|
@ -1,33 +0,0 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
interface ImportMetaEnv {
|
||||
VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface MoneroNode {
|
||||
id: number;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
protocol: string;
|
||||
is_tor: boolean;
|
||||
is_available: boolean;
|
||||
nettype: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: null | object | object[];
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -1,13 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="false" data-theme="skeleton">
|
||||
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,14 +0,0 @@
|
|||
<script>
|
||||
import { version } from '$app/environment';
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-end border-t border-surface-500/10 bg-surface-50 dark:bg-surface-900">
|
||||
<footer class="w-full">
|
||||
<div class="bg-surface-500/5">
|
||||
<div class="container mx-auto px-5 py-4">
|
||||
<!-- prettier-ignore -->
|
||||
<p class="text-center text-sm">XMR Nodes {version}, <a href="https://github.com/ditatompel/xmr-remote-nodes" target="_blank" rel="noopener" class="external">source code</a> licensed under <strong>GLWTPL</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
|
@ -1,58 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
export let handler: DataHandler<T>;
|
||||
|
||||
let intervalId: number | undefined;
|
||||
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) {
|
||||
handler.invalidate();
|
||||
intervalId = setInterval(() => {
|
||||
handler.invalidate();
|
||||
}, seconds * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
$: startInterval();
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<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>
|
|
@ -1,69 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
|
||||
const pageNumber = handler.getPageNumber();
|
||||
const pageCount = handler.getPageCount();
|
||||
const pages = handler.getPages({ ellipsis: true });
|
||||
|
||||
const setPage = (value: 'previous' | 'next' | number) => {
|
||||
handler.setPage(value);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class={$$props.class ?? ''}>
|
||||
{#if $pages === undefined}
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('next')}>❯</button>
|
||||
{:else}
|
||||
<div class="lg:hidden">
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||
<button
|
||||
class="sm-btn"
|
||||
class:disabled={$pageNumber === $pageCount}
|
||||
on:click={() => setPage('next')}
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group variant-ghost-surface hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
class="hover:variant-soft-secondary"
|
||||
class:disabled={$pageNumber === 1}
|
||||
on:click={() => setPage('previous')}>❮</button
|
||||
>
|
||||
{#each $pages as page}<button
|
||||
type="button"
|
||||
class="hover:variant-filled-secondary"
|
||||
class:!variant-filled-primary={$pageNumber === page}
|
||||
class:ellipse={page === null}
|
||||
on:click={() => setPage(page)}>{page ?? '...'}</button
|
||||
>{/each}
|
||||
<button
|
||||
type="button"
|
||||
class="hover:variant-soft-secondary"
|
||||
class:disabled={$pageNumber === $pageCount}
|
||||
on:click={() => setPage('next')}
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
.sm-btn {
|
||||
@apply btn btn-sm variant-ghost-surface hover:variant-soft-secondary;
|
||||
}
|
||||
.disabled {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
const rowCount = handler.getRowCount();
|
||||
</script>
|
||||
|
||||
{#if $rowCount === undefined}
|
||||
<div />
|
||||
{:else}
|
||||
<div class={$$props.class ?? 'mr-6 leading-8 lg:leading-10'}>
|
||||
{#if $rowCount.total > 0}
|
||||
<b>{$rowCount.start}</b>
|
||||
- <b>{$rowCount.end}</b>
|
||||
/ <b>{$rowCount.total}</b>
|
||||
{:else}
|
||||
No entries found
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -1,33 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let options = [5, 10, 20, 50, 100];
|
||||
export let labelId = 'rowsPerPage';
|
||||
|
||||
const rowsPerPage = handler.getRowsPerPage();
|
||||
|
||||
const setRowsPerPage = () => {
|
||||
handler.setPage(1);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex place-items-center">
|
||||
<label for={labelId}>Show</label>
|
||||
<select
|
||||
class="select ml-2"
|
||||
id={labelId}
|
||||
name="rowsPerPage"
|
||||
bind:value={$rowsPerPage}
|
||||
on:change={setRowsPerPage}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler } from '@vincjo/datatables/remote';
|
||||
export let handler: DataHandler;
|
||||
let value: string;
|
||||
let timeout: any;
|
||||
|
||||
const search = () => {
|
||||
handler.search(value);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
handler.invalidate();
|
||||
}, 400);
|
||||
};
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="input-variant-secondary input w-36 sm:w-64"
|
||||
type="search"
|
||||
name="tableGlobalSearch"
|
||||
placeholder="Search..."
|
||||
bind:value
|
||||
on:input={search}
|
||||
/>
|
|
@ -1,35 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let filterBy: keyof T;
|
||||
|
||||
/** @type {string} */
|
||||
export let placeholder: string = 'Filter';
|
||||
|
||||
/** @type {number} */
|
||||
export let colspan: number = 1;
|
||||
|
||||
let value: string = '';
|
||||
let timeout: any;
|
||||
|
||||
const filter = () => {
|
||||
handler.filter(value, filterBy);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
handler.invalidate();
|
||||
}, 400);
|
||||
};
|
||||
</script>
|
||||
|
||||
<th {colspan}>
|
||||
<input
|
||||
class="input variant-form-material h-8 w-full text-sm"
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:input={filter}
|
||||
/>
|
||||
</th>
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let orderBy: keyof T;
|
||||
|
||||
const update = () => {
|
||||
handler.sort(orderBy);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<th on:click={update} class="cursor-pointer select-none p-2 px-5">
|
||||
<div class="flex h-full items-center justify-start gap-x-2">
|
||||
<slot /> ↕️
|
||||
</div>
|
||||
</th>
|
|
@ -1,7 +0,0 @@
|
|||
export { default as DtSrPagination } from './DtSrPagination.svelte';
|
||||
export { default as DtSrRowCount } from './DtSrRowCount.svelte';
|
||||
export { default as DtSrRowsPerPage } from './DtSrRowsPerPage.svelte';
|
||||
export { default as DtSrSearch } from './DtSrSearch.svelte';
|
||||
export { default as DtSrThFilter } from './DtSrThFilter.svelte';
|
||||
export { default as DtSrThSort } from './DtSrThSort.svelte';
|
||||
export { default as DtSrAutoRefresh } from './DtSrAutoRefresh.svelte';
|
|
@ -1,39 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let cc: string;
|
||||
export let country_name: string;
|
||||
export let city: string;
|
||||
export let asn: number;
|
||||
export let asn_name: string;
|
||||
|
||||
$: lowerCc = cc.toLowerCase();
|
||||
</script>
|
||||
|
||||
{#if cc != ''}
|
||||
{#if city !== ''}
|
||||
{city},
|
||||
{/if}
|
||||
{country_name}
|
||||
<img class="inline-block" src="/img/cf/{lowerCc}.svg" alt="{cc} Flag" width="22px" />
|
||||
{/if}
|
||||
|
||||
{#if asn !== 0}
|
||||
<br /><a
|
||||
class="external asn"
|
||||
href="https://www.ditatompel.com/asn/{asn}"
|
||||
target="_blank"
|
||||
rel="noopener">AS{asn}</a
|
||||
>
|
||||
(<span class="asn-name">{asn_name}</span>)
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
a {
|
||||
@apply font-semibold text-sky-800 underline dark:text-sky-500;
|
||||
}
|
||||
a.asn {
|
||||
@apply !text-purple-800 dark:!text-purple-400;
|
||||
}
|
||||
span.asn-name {
|
||||
@apply font-semibold text-green-800 dark:text-green-500;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let estimate_fee: number;
|
||||
export let majority_fee: number;
|
||||
</script>
|
||||
|
||||
{#if estimate_fee !== majority_fee}
|
||||
<span class="text-orange-800 dark:text-orange-300">{estimate_fee}<br />(CAUTION!)</span>
|
||||
{:else}
|
||||
{estimate_fee}
|
||||
{/if}
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import { getModalStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const modalStore = getModalStore();
|
||||
/** @type {string} */
|
||||
export let ip;
|
||||
/** @type {boolean} */
|
||||
export let is_tor;
|
||||
/** @type {string} */
|
||||
export let hostname;
|
||||
/** @type {number} */
|
||||
export let port;
|
||||
|
||||
// if (is_tor) {
|
||||
// hostname = hostname.substring(0, 8) + '[...].onion';
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param {string} onionAddr
|
||||
* @param {number} port
|
||||
*/
|
||||
function modalAlert(onionAddr, port) {
|
||||
/** @typedef {import('@skeletonlabs/skeleton').ModalSettings} ModalSettings */
|
||||
/** @type {ModalSettings} */
|
||||
const modal = {
|
||||
type: 'alert',
|
||||
title: 'Hostname:',
|
||||
body: '<code class="code">' + onionAddr + ':' + port + '</code>'
|
||||
};
|
||||
modalStore.trigger(modal);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if is_tor}
|
||||
<button
|
||||
class="max-w-32 truncate text-orange-800 dark:text-orange-300"
|
||||
on:click={() => modalAlert(hostname, port)}
|
||||
>
|
||||
👁 {hostname}
|
||||
</button><br />.onion:<span class="text-indigo-800 dark:text-indigo-400">{port}</span>
|
||||
<span class="text-gray-700 dark:text-gray-400">(TOR)</span>
|
||||
{:else}
|
||||
{hostname}:<span class="text-indigo-800 dark:text-indigo-400">{port}</span>
|
||||
{#if ip !== ''}
|
||||
<br /><span class="text-gray-700 dark:text-gray-400">{ip}</span>
|
||||
{/if}
|
||||
{/if}
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
/** @type {string} */
|
||||
export let nettype;
|
||||
/** @type {number} */
|
||||
export let height;
|
||||
</script>
|
||||
|
||||
{#if nettype === 'stagenet'}
|
||||
<span class="font-semibold uppercase text-sky-800 dark:text-sky-500">{nettype}</span>
|
||||
{:else if nettype === 'testnet'}
|
||||
<span class="font-semibold uppercase text-rose-800 dark:text-rose-400">{nettype}</span>
|
||||
{:else}
|
||||
<span class="font-semibold uppercase text-green-800 dark:text-green-500">{nettype}</span>
|
||||
{/if}
|
||||
<br />{height}
|
|
@ -1,16 +0,0 @@
|
|||
<script>
|
||||
/** @type {string} */
|
||||
export let protocol;
|
||||
/** @type {boolean} */
|
||||
export let cors;
|
||||
</script>
|
||||
|
||||
{#if protocol === 'http'}
|
||||
<span class="font-semibold uppercase text-sky-800 dark:text-sky-500">{protocol}</span>
|
||||
{:else}
|
||||
<span class="font-semibold uppercase text-green-800 dark:text-green-500">{protocol}</span>
|
||||
{/if}
|
||||
|
||||
{#if cors}
|
||||
<br />(CORS 💪)
|
||||
{/if}
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name="filterAnonymity" class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === true}
|
||||
<option {value}>TOR</option>
|
||||
{:else}
|
||||
<option {value}>CLEARNET</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<script>
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
/** @type {string} */
|
||||
export let filterName;
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name={filterName} class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === ''}
|
||||
<option {value}>UNKNOWN</option>
|
||||
{:else}
|
||||
<option {value}>{value.toUpperCase()}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name="filterStatus" class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === true}
|
||||
<option {value}>ONLINE</option>
|
||||
{:else}
|
||||
<option {value}>OFFLINE</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let is_available: boolean;
|
||||
export let statuses: number[];
|
||||
</script>
|
||||
|
||||
{#if is_available}
|
||||
<span class="font-semibold text-green-800 dark:text-green-500">Online</span>
|
||||
{:else}
|
||||
<span class="text-rose-800 dark:text-rose-400">Offline</span>
|
||||
{/if}
|
||||
<br />
|
||||
{#each statuses as status}
|
||||
{#if status === 1}
|
||||
<span class="text-success-700 dark:text-success-400 mr-1">•</span>
|
||||
{:else if status === 0}
|
||||
<span class="text-error-700 dark:text-error-400 mr-1">•</span>
|
||||
{:else}
|
||||
<span class="text-surface-400 dark:text-surface-600 mr-1">•</span>
|
||||
{/if}
|
||||
{/each}
|
|
@ -1,13 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let uptime: number;
|
||||
</script>
|
||||
|
||||
{#if uptime >= 98}
|
||||
<span class="text-green-800 dark:text-green-500">{uptime}%</span>
|
||||
{:else if uptime < 98 && uptime >= 80}
|
||||
<span class="text-sky-800 dark:text-sky-500">{uptime}%</span>
|
||||
{:else if uptime < 80 && uptime > 75}
|
||||
<span class="text-orange-800 dark:text-orange-300">{uptime}%</span>
|
||||
{:else}
|
||||
<span class="text-rose-800 dark:text-rose-400">{uptime}%</span>
|
||||
{/if}
|
|
@ -1,10 +0,0 @@
|
|||
export { default as CountryCellWithAsn } from './CountryCellWithAsn.svelte';
|
||||
export { default as EstimateFeeCell } from './EstimateFeeCell.svelte';
|
||||
export { default as HostPortCell } from './HostPortCell.svelte';
|
||||
export { default as NetTypeCell } from './NetTypeCell.svelte';
|
||||
export { default as ProtocolCell } from './ProtocolCell.svelte';
|
||||
export { default as SelectAnonymityFilter } from './SelectAnonymityFilter.svelte';
|
||||
export { default as SelectFilter } from './SelectFilter.svelte';
|
||||
export { default as SelectStatusFilter } from './SelectStatusFilter.svelte';
|
||||
export { default as StatusCell } from './StatusCell.svelte';
|
||||
export { default as UptimeCell } from './UptimeCell.svelte';
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { adminNavs } from './navs';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
$: style = (/** @type {string} */ href) =>
|
||||
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
|
||||
</script>
|
||||
|
||||
<nav class="list-nav p-4">
|
||||
<ul>
|
||||
{#each adminNavs as nav}
|
||||
<li>
|
||||
<a href={nav.path} class={style(nav.path)} on:click={() => drawerStore.close()}
|
||||
>{nav.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,72 +0,0 @@
|
|||
<script>
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
/** @type {ApiResponse} */
|
||||
let formResult;
|
||||
|
||||
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||
async function handleLogout(event) {
|
||||
const data = new FormData(event.currentTarget);
|
||||
|
||||
const response = await fetch(event.currentTarget.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.fromEntries(data))
|
||||
});
|
||||
|
||||
formResult = await response.json();
|
||||
|
||||
if (formResult.status === 'ok') {
|
||||
await invalidateAll();
|
||||
goto('/login/');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-surface-100-800-token fixed top-0 z-30 w-full shadow-2xl">
|
||||
<div class="px-3 py-2 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<button
|
||||
class="btn btn-sm inline-flex items-center md:hidden"
|
||||
aria-label="Mobile Drawer Button"
|
||||
on:click={() => drawerStore.open({})}
|
||||
>
|
||||
<span>
|
||||
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
|
||||
<rect width="100" height="20" />
|
||||
<rect y="30" width="100" height="20" />
|
||||
<rect y="60" width="100" height="20" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<a href="/app/prober/" class="ms-2 flex md:me-24" aria-label="title">
|
||||
<span class="hidden self-center whitespace-nowrap text-2xl font-semibold lg:block"
|
||||
>XMR Nodes</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="ms-3 flex items-center space-x-4">
|
||||
<LightSwitch />
|
||||
<form
|
||||
action={apiUri('/auth/logout')}
|
||||
method="POST"
|
||||
on:submit|preventDefault={handleLogout}
|
||||
>
|
||||
<input type="hidden" name="logout" value="logout" />
|
||||
<button type="submit" class="btn btn-sm variant-filled-error" role="menuitem">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,34 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { adminNavs } from './navs';
|
||||
</script>
|
||||
|
||||
<aside
|
||||
id="logo-sidebar"
|
||||
class="bg-surface-100-800-token fixed left-0 top-0 z-20 h-screen w-64 -translate-x-full pt-20 shadow-2xl transition-transform sm:translate-x-0"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div class="h-full overflow-y-auto px-3 pb-4">
|
||||
<ul class="space-y-2 font-medium list-none" data-sveltekit-preload-data="false">
|
||||
{#each adminNavs as nav}
|
||||
<li>
|
||||
<a
|
||||
href={nav.path}
|
||||
class={$page.url.pathname.startsWith(nav.path) ? 'active' : 'nav-link'}
|
||||
>
|
||||
<span class="ms-3">{nav.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style lang="postcss">
|
||||
.active {
|
||||
@apply flex items-center rounded-lg bg-primary-500 p-2;
|
||||
}
|
||||
.nav-link {
|
||||
@apply flex items-center rounded-lg p-2 hover:bg-secondary-500 hover:text-white;
|
||||
}
|
||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navs } from './navs';
|
||||
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
</script>
|
||||
|
||||
<nav class="fixed w-full z-20 top-0 start-0 bg-surface-100-800-token shadow-2xl">
|
||||
<div class="mx-auto flex max-w-screen-xl flex-wrap items-center justify-between px-4 py-1">
|
||||
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse" aria-label="xmr nodes">
|
||||
<span class="self-center whitespace-nowrap text-2xl font-semibold lg:block">XMR Nodes</span>
|
||||
</a>
|
||||
<div class="flex items-center space-x-1 md:order-2 md:space-x-0 rtl:space-x-reverse">
|
||||
<LightSwitch />
|
||||
<button
|
||||
class="btn btn-sm mr-4 md:hidden"
|
||||
aria-label="Mobile Drawer Button"
|
||||
on:click={() => drawerStore.open({})}
|
||||
>
|
||||
<span>
|
||||
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
|
||||
<rect width="100" height="20" />
|
||||
<rect y="30" width="100" height="20" />
|
||||
<rect y="60" width="100" height="20" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto">
|
||||
<ul
|
||||
class="flex flex-row space-x-1 rounded-lg bg-white p-0 dark:bg-gray-900 rtl:space-x-reverse"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
class={$page.url.pathname === '/' ? 'active' : 'nav-link'}
|
||||
aria-current={$page.url.pathname === '/' ? 'page' : undefined}>Home</a
|
||||
>
|
||||
</li>
|
||||
{#each navs as nav}
|
||||
<li>
|
||||
<a
|
||||
href={nav.path}
|
||||
class={$page.url.pathname.startsWith(nav.path) ? 'active' : 'nav-link'}
|
||||
>
|
||||
{nav.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="postcss">
|
||||
.active {
|
||||
@apply block rounded bg-primary-500 p-2 text-black;
|
||||
}
|
||||
.nav-link {
|
||||
@apply block rounded hover:bg-secondary-500 md:p-2 hover:text-white;
|
||||
}
|
||||
</style>
|
|
@ -1,28 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navs } from './navs';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
$: classes = (/** @type {string} */ href) =>
|
||||
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
|
||||
</script>
|
||||
|
||||
<nav class="list-nav p-4">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
class={$page.url.pathname === '/' ? 'bg-primary-500' : ''}
|
||||
on:click={() => drawerStore.close()}>Home</a
|
||||
>
|
||||
</li>
|
||||
{#each navs as nav}
|
||||
<li>
|
||||
<a href={nav.path} class={classes(nav.path)} on:click={() => drawerStore.close()}
|
||||
>{nav.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,5 +0,0 @@
|
|||
export { default as MainNav } from './MainNav.svelte';
|
||||
export { default as MobileDrawer } from './MobileDrawer.svelte';
|
||||
export { default as AdminNav } from './AdminNav.svelte';
|
||||
export { default as AdminSidebar } from './AdminSidebar.svelte';
|
||||
export { default as AdminMobileDrawer } from './AdminMobileDrawer.svelte';
|
|
@ -1,9 +0,0 @@
|
|||
export const adminNavs = [
|
||||
{ name: 'Prober', path: '/app/prober/' },
|
||||
{ name: 'Crons', path: '/app/crons/' }
|
||||
];
|
||||
|
||||
export const navs = [
|
||||
{ name: 'Remote Nodes', path: '/remote-nodes/' },
|
||||
{ name: 'Add Node', path: '/add-node/' }
|
||||
];
|
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5V334.2H141.4V256h52.8V222.3c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287V510.1C413.8 494.8 512 386.9 512 256h0z"
|
||||
/></svg
|
||||
>
|
Before (image error) Size: 582 B |
|
@ -1,10 +0,0 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||
></path>
|
||||
</svg>
|
Before (image error) Size: 880 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 496 512"
|
||||
><path
|
||||
d="M352 384h108.4C417 455.9 338.1 504 248 504S79 455.9 35.6 384H144V256.2L248 361l104-105v128zM88 336V128l159.4 159.4L408 128v208h74.8c8.5-25.1 13.2-52 13.2-80C496 119 385 8 248 8S0 119 0 256c0 28 4.6 54.9 13.2 80H88z"
|
||||
/></svg
|
||||
>
|
Before (image error) Size: 528 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M373 138.6c-25.2 0-46.3-17.5-51.9-41l0 0c-30.6 4.3-54.2 30.7-54.2 62.4l0 .2c47.4 1.8 90.6 15.1 124.9 36.3c12.6-9.7 28.4-15.5 45.5-15.5c41.3 0 74.7 33.4 74.7 74.7c0 29.8-17.4 55.5-42.7 67.5c-2.4 86.8-97 156.6-213.2 156.6S45.5 410.1 43 323.4C17.6 311.5 0 285.7 0 255.7c0-41.3 33.4-74.7 74.7-74.7c17.2 0 33 5.8 45.7 15.6c34-21.1 76.8-34.4 123.7-36.4l0-.3c0-44.3 33.7-80.9 76.8-85.5C325.8 50.2 347.2 32 373 32c29.4 0 53.3 23.9 53.3 53.3s-23.9 53.3-53.3 53.3zM157.5 255.3c-20.9 0-38.9 20.8-40.2 47.9s17.1 38.1 38 38.1s36.6-9.8 37.8-36.9s-14.7-49.1-35.7-49.1zM395 303.1c-1.2-27.1-19.2-47.9-40.2-47.9s-36.9 22-35.7 49.1c1.2 27.1 16.9 36.9 37.8 36.9s39.3-11 38-38.1zm-60.1 70.8c1.5-3.6-1-7.7-4.9-8.1c-23-2.3-47.9-3.6-73.8-3.6s-50.8 1.3-73.8 3.6c-3.9 .4-6.4 4.5-4.9 8.1c12.9 30.8 43.3 52.4 78.7 52.4s65.8-21.6 78.7-52.4z"
|
||||
/></svg
|
||||
>
|
Before (image error) Size: 1.1 KiB |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 496 512"
|
||||
><path
|
||||
d="M248 8C111 8 0 119 0 256S111 504 248 504 496 393 496 256 385 8 248 8zM363 176.7c-3.7 39.2-19.9 134.4-28.1 178.3-3.5 18.6-10.3 24.8-16.9 25.4-14.4 1.3-25.3-9.5-39.3-18.7-21.8-14.3-34.2-23.2-55.3-37.2-24.5-16.1-8.6-25 5.3-39.5 3.7-3.8 67.1-61.5 68.3-66.7 .2-.7 .3-3.1-1.2-4.4s-3.6-.8-5.1-.5q-3.3 .7-104.6 69.1-14.8 10.2-26.9 9.9c-8.9-.2-25.9-5-38.6-9.1-15.5-5-27.9-7.7-26.8-16.3q.8-6.7 18.5-13.7 108.4-47.2 144.6-62.3c68.9-28.6 83.2-33.6 92.5-33.8 2.1 0 6.6 .5 9.6 2.9a10.5 10.5 0 0 1 3.5 6.7A43.8 43.8 0 0 1 363 176.7z"
|
||||
/></svg
|
||||
>
|
Before (image error) Size: 831 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"
|
||||
/></svg
|
||||
>
|
Before (image error) Size: 470 B |
|
@ -1,6 +0,0 @@
|
|||
export { default as IcnGitHub } from './IcnGitHub.svelte';
|
||||
export { default as IcnMonero } from './IcnMonero.svelte';
|
||||
export { default as IcnReddit } from './IcnReddit.svelte';
|
||||
export { default as IcnTwitter } from './IcnTwitter.svelte';
|
||||
export { default as IcnFacebook } from './IcnFacebook.svelte';
|
||||
export { default as IcnTelegram } from './IcnTelegram.svelte';
|
|
@ -1 +0,0 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -1,17 +0,0 @@
|
|||
export const getDistinct = (items) => {
|
||||
return Array.from(getCounter(items).keys());
|
||||
};
|
||||
|
||||
export const getDuplicates = (items) => {
|
||||
return Array.from(getCounter(items).entries())
|
||||
.filter(([, count]) => count !== 1)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
export const getCounter = (items) => {
|
||||
const result = new Map();
|
||||
items.forEach((item) => {
|
||||
result.set(item, (result.get(item) ?? 0) + 1);
|
||||
});
|
||||
return result;
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
/** @param {string} path */
|
||||
export const apiUri = (path) => {
|
||||
return `${import.meta.env.VITE_API_URL || ''}${path}`;
|
||||
};
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* @param {number} bytes
|
||||
* @param {number} decimals
|
||||
* @returns {string}
|
||||
*/
|
||||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (!+bytes) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a number with a maximum precision.
|
||||
*
|
||||
* This function was copied from jtgrassie/monero-pool project.
|
||||
* Source: https://github.com/jtgrassie/monero-pool/blob/master/src/webui-embed.html
|
||||
*
|
||||
* Copyright (c) 2018, The Monero Project
|
||||
*
|
||||
* @param {number} n
|
||||
* @param {number} p
|
||||
*/
|
||||
const maxPrecision = (n, p) => {
|
||||
return parseFloat(n.toFixed(p));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a hash value (h) into human readable format.
|
||||
*
|
||||
* This function was copied from jtgrassie/monero-pool project.
|
||||
* Source: https://github.com/jtgrassie/monero-pool/blob/master/src/webui-embed.html
|
||||
*
|
||||
* Copyright (c) 2018, The Monero Project
|
||||
*
|
||||
* @param {number} h
|
||||
*/
|
||||
export const formatHashes = (h) => {
|
||||
if (h < 1e-12) return '0 H';
|
||||
else if (h < 1e-9) return maxPrecision(h * 1e12, 0) + ' pH';
|
||||
else if (h < 1e-6) return maxPrecision(h * 1e9, 0) + ' nH';
|
||||
else if (h < 1e-3) return maxPrecision(h * 1e6, 0) + ' μH';
|
||||
else if (h < 1) return maxPrecision(h * 1e3, 0) + ' mH';
|
||||
else if (h < 1e3) return h + ' H';
|
||||
else if (h < 1e6) return maxPrecision(h * 1e-3, 2) + ' KH';
|
||||
else if (h < 1e9) return maxPrecision(h * 1e-6, 2) + ' MH';
|
||||
else return maxPrecision(h * 1e-9, 2) + ' GH';
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
|
@ -1,85 +0,0 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
Toast,
|
||||
Modal,
|
||||
Drawer,
|
||||
ProgressBar,
|
||||
initializeStores,
|
||||
storePopup
|
||||
} from '@skeletonlabs/skeleton';
|
||||
import { beforeNavigate, afterNavigate } from '$app/navigation';
|
||||
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
|
||||
import { MainNav, MobileDrawer } from '$lib/components/navigation';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
initializeStores();
|
||||
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||
let isLoading = false;
|
||||
|
||||
beforeNavigate(() => (isLoading = true));
|
||||
afterNavigate(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
/* prettier-ignore */
|
||||
const meta = {
|
||||
title: 'Monero Remote Node',
|
||||
description: 'A website that helps you monitor your favourite Monero remote nodes, a device on the internet running the Monero software with copy of the Monero blockchain.',
|
||||
keywords: 'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet'
|
||||
};
|
||||
|
||||
page.subscribe((page) => {
|
||||
if (typeof page.data.meta === 'object') {
|
||||
meta.title = page.data.meta.title ?? meta.title;
|
||||
meta.description = page.data.meta.description ?? meta.description;
|
||||
meta.keywords = page.data.meta.keywords ?? meta.description;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{meta.title} — xmr.ditatompel.com</title>
|
||||
<!-- Meta Tags -->
|
||||
<meta name="title" content="{meta.title} — xmr.ditatompel.com" />
|
||||
<meta name="description" content={meta.description} />
|
||||
<meta name="keywords" content={meta.keywords} />
|
||||
<meta name="theme-color" content="#272b31" />
|
||||
<meta name="author" content="ditatompel" />
|
||||
|
||||
<!-- Open Graph - https://ogp.me/ -->
|
||||
<meta property="og:site_name" content="xmr.ditatompel.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://xmr.ditatompel.com{$page.url.pathname}" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:title" content="{meta.title} — xmr.ditatompel.com" />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<Modal />
|
||||
<Toast />
|
||||
|
||||
{#if isLoading}
|
||||
<ProgressBar
|
||||
class="fixed top-0 z-50"
|
||||
height="h-1"
|
||||
track="bg-opacity-100"
|
||||
meter="bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Drawer>
|
||||
<h2 class="p-4">Navigation</h2>
|
||||
<hr />
|
||||
<MobileDrawer />
|
||||
<hr />
|
||||
</Drawer>
|
||||
|
||||
<MainNav />
|
||||
|
||||
<div class="pt-10 md:pt-12 min-h-screen">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Footer />
|
|
@ -1,36 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
return {
|
||||
meta: {
|
||||
title: 'Monero Remote Node',
|
||||
description:
|
||||
'A website that helps you monitor your favourite Monero remote nodes, a device on the internet running the Monero software with copy of the Monero blockchain.',
|
||||
keywords:
|
||||
'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet'
|
||||
},
|
||||
links: [
|
||||
{ text: 'moneroworld.com', uri: 'https://moneroworld.com' },
|
||||
{ text: 'monero.how', uri: 'https://www.monero.how' },
|
||||
{ text: 'monero.observer', uri: 'https://www.monero.observer' },
|
||||
{ text: 'sethforprivacy.com', uri: 'https://sethforprivacy.com' },
|
||||
{ text: 'localmonero.co', uri: 'https://localmonero.co/knowledge' },
|
||||
{ text: 'revuo-xmr.com', uri: 'https://revuo-xmr.com/' }
|
||||
],
|
||||
stagenet: [
|
||||
{ label: 'P2P', value: 'stagenet.xmr.ditatompel.com:38080', key: 'snetP2P' },
|
||||
{ label: 'RPC', value: 'stagenet.xmr.ditatompel.com:38089', key: 'snetRPC' },
|
||||
{ label: 'RPC SSL', value: 'stagenet.xmr.ditatompel.com:443', key: 'snetSSL' }
|
||||
],
|
||||
testnet: [
|
||||
{ label: 'P2P', value: 'testnet.xmr.ditatompel.com:28080', key: 'tnetP2P' },
|
||||
{ label: 'RPC', value: 'testnet.xmr.ditatompel.com:28089', key: 'tnetRPC' },
|
||||
{ label: 'RPC SSL', value: 'testnet.xmr.ditatompel.com:443', key: 'tnetSSL' }
|
||||
],
|
||||
donation: {
|
||||
// You change donation address and qr image below if you run your own "instance"
|
||||
address:
|
||||
'8BWYe6GzbNKbxe3D8mPkfFMQA2rViaZJFhWShhZTjJCNG6EZHkXRZCKHiuKmwwe4DXDYF8KKcbGkvNYaiRG3sNt7JhnVp7D',
|
||||
qr: '/img/monerotip.png'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
<script>
|
||||
import { clipboard } from '@skeletonlabs/skeleton';
|
||||
import {
|
||||
IcnGitHub,
|
||||
IcnMonero,
|
||||
IcnReddit,
|
||||
IcnTwitter,
|
||||
IcnFacebook,
|
||||
IcnTelegram
|
||||
} from '$lib/components/svg';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
let donationCopied = false;
|
||||
|
||||
/** @param {Event & { target: HTMLInputElement }} e */
|
||||
function copyHandler(e) {
|
||||
e.target.disabled = true;
|
||||
e.target.innerText = 'Copied 👍';
|
||||
setTimeout(() => {
|
||||
e.target.innerText = 'Copy';
|
||||
e.target.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function copyDonationAddr() {
|
||||
donationCopied = true;
|
||||
setTimeout(() => {
|
||||
donationCopied = false;
|
||||
}, 2500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<p>{data.meta.description}</p>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<div class="pt-2">
|
||||
<a href="https://www.getmonero.org" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnMonero class="h-4 w-4" /></span>
|
||||
<span>GetMonero.org</span>
|
||||
</a>
|
||||
<a href="https://github.com/monero-project" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnGitHub fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monero-project</span>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/Monero/" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnReddit fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>r/Monero</span>
|
||||
</a>
|
||||
<a href="https://twitter.com/monero" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnTwitter fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>@monero</span>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/monerocurrency/" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnFacebook fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monerocurrency</span>
|
||||
</a>
|
||||
<a href="https://telegram.me/monero" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnTelegram fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monero</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="introduction">
|
||||
<div class="section-container text-center">
|
||||
<p>If you're new to Monero, the official links above is a perfect place to start.</p>
|
||||
<p class="py-2">
|
||||
Of course, there are lots of personal and community sites which generally discusses a lot
|
||||
about Monero, such as
|
||||
{#each data.links as link}
|
||||
<a href={link.uri} class="external" target="_blank" rel="noopener">{link.text}</a>,
|
||||
{/each} etc; can be an other good reference for you.
|
||||
</p>
|
||||
<p>You can find few resources I provide related to Monero below:</p>
|
||||
</div>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<div class="section-container text-token grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="/remote-nodes/">
|
||||
<h2 class="h2 font-bold">Remote Nodes</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>List of Monero remote nodes you can use when you cannot (or don't want to) run your own node.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="/add-node/">
|
||||
<h2 class="h2 font-bold">Add Node</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>Add your Monero public node so others can connect to your node.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="https://monitor.ditatompel.com/d/xmr_metrics/monero-metrics?orgId=2" target="_blank" rel="noopener" >
|
||||
<h2 class="h2 font-bold">Metrics</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>Collection of my Monero metrics (GitHub repository, blockchain, market, P2Pool) presented through Grafana. ↗</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="my-monero-public-nodes" class="bg-surface-100-800-token">
|
||||
<div class="section-container text-token grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div class="text-center">
|
||||
<h2 class="h2 pb-2 font-bold">My Stagenet Public Node</h2>
|
||||
<p>
|
||||
Stagenet is what you need to learn Monero safely. Stagenet is technically equivalent to
|
||||
mainnet, both in terms of features and consensus rules.
|
||||
</p>
|
||||
{#each data.stagenet as { label, value, key }}
|
||||
<div class="input-group input-group-divider my-2 grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim"><label for={key}>{label}</label></div>
|
||||
<input class="text-center" type="text" id={key} name={key} {value} data-clipboard={key} />
|
||||
<button
|
||||
class="variant-filled-secondary"
|
||||
use:clipboard={{ input: key }}
|
||||
on:click={copyHandler}>Copy</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<h2 class="h2 pb-2 font-bold">My Testnet Public Node</h2>
|
||||
<p>
|
||||
Testnet is the <em>"experimental"</em> network and blockchain where things get released long
|
||||
before mainnet. As a normal user, use mainnet instead.
|
||||
</p>
|
||||
{#each data.testnet as { label, value, key }}
|
||||
<div class="input-group input-group-divider my-2 grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim"><label for={key}>{label}</label></div>
|
||||
<input class="text-center" type="text" id={key} name={key} {value} data-clipboard={key} />
|
||||
<button
|
||||
class="variant-filled-secondary"
|
||||
use:clipboard={{ input: key }}
|
||||
on:click={copyHandler}>Copy</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="privacy-quote">
|
||||
<div class="text-token mx-auto w-full max-w-4xl py-4 text-center">
|
||||
<!-- prettier-ignore -->
|
||||
<blockquote class="blockquote">
|
||||
<p class="text-3xl">
|
||||
Since we desire privacy, we must ensure that each party to a transaction have knowledge only of that which is directly necessary for that transaction.
|
||||
</p>
|
||||
<p class="my-2">
|
||||
<strong>Eric Hughes</strong> in <a href="https://www.activism.net/cypherpunk/manifesto.html" class="external" target="_blank" rel="noopener"><cite title="Source Title">A Cypherpunk's Manifesto</cite></a>.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="monero-donation" class="section-container text-token text-center">
|
||||
<div class="mx-auto flex w-full max-w-4xl flex-row items-center gap-10">
|
||||
<div class="md:basis-3/4">
|
||||
<label for="donate">If you like to buy me a coffee, here is my Monero address:</label>
|
||||
<textarea class="textarea my-2" id="donate" name="donate" data-clipboard="donate" readonly
|
||||
>{data.donation.address}</textarea
|
||||
>
|
||||
<button
|
||||
class="variant-filled-success btn"
|
||||
use:clipboard={{ input: 'donate' }}
|
||||
disabled={donationCopied}
|
||||
on:click={copyDonationAddr}
|
||||
>{donationCopied ? 'Donation Address Copied! 🤩' : 'Copy Donation Address'}</button
|
||||
>
|
||||
</div>
|
||||
<div class="md:basis-1/4">
|
||||
<img src={data.donation.qr} alt="ditatompel's monero address" />
|
||||
<p>Thank you so much! It means a lot to me. 🥰</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,12 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
/* prettier-ignore */
|
||||
return {
|
||||
meta: {
|
||||
title: 'Add Monero Node',
|
||||
description:
|
||||
'You can use this page to add known remote node to the system so my bots can monitor it.',
|
||||
keywords: 'monero,monero node,monero public node,monero wallet,list monero node,monero node monitoring'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
<script>
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import { apiUri } from '$lib/utils/common';
|
||||
import { ProgressBar } from '@skeletonlabs/skeleton';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
/** @type {ApiResponse} */
|
||||
export let formResult;
|
||||
|
||||
let isProcessing = false;
|
||||
|
||||
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||
async function handleSubmit(event) {
|
||||
isProcessing = true;
|
||||
const data = new FormData(event.currentTarget);
|
||||
|
||||
const response = await fetch(event.currentTarget.action, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
|
||||
formResult = await response.json();
|
||||
isProcessing = false;
|
||||
|
||||
if (formResult.status === 'ok') {
|
||||
await invalidateAll();
|
||||
goto('/remote-nodes');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<p>{data.meta.description}</p>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="form-add-monero-node">
|
||||
<div class="section-container text-center">
|
||||
<p>Enter your Monero node information below (IPv4 host only):</p>
|
||||
|
||||
<form
|
||||
class="mx-auto w-full max-w-3xl py-2"
|
||||
action={apiUri('/api/v1/nodes')}
|
||||
method="POST"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 py-6 md:grid-cols-4">
|
||||
<label class="label">
|
||||
<span>Protocol *</span>
|
||||
<select name="protocol" class="select variant-form-material" disabled={isProcessing}>
|
||||
<option value="http">HTTP / TOR</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Host / IP *</span>
|
||||
<input
|
||||
class="input variant-form-material"
|
||||
name="hostname"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Eg: node.example.com or 172.16.17.18"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Port *</span>
|
||||
<input
|
||||
class="input variant-form-material"
|
||||
name="port"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Eg: 18081"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button class="variant-filled-success btn" disabled={isProcessing}
|
||||
>{isProcessing ? 'Processing...' : 'Submit'}</button
|
||||
>
|
||||
</form>
|
||||
|
||||
<div class="mx-auto w-full max-w-3xl py-2">
|
||||
{#if !isProcessing}
|
||||
{#if formResult?.status === 'error'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-red-400" role="alert">
|
||||
<span class="font-medium">Error:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{#if formResult?.status === 'ok'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-green-400" role="alert">
|
||||
<span class="font-medium">Success:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-blue-400" role="alert">
|
||||
<span class="font-medium">Processing...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Here you can find list of <a class="anchor" href="/remote-nodes/">Monero Remote Node</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
return {
|
||||
// prettier-ignore
|
||||
meta: {
|
||||
title: 'Public Monero Remote Nodes List',
|
||||
description: 'List of public Monero remote nodes that you can use with your favourite Monero wallet. You can filter by country, protocol, or CORS capable nodes.',
|
||||
keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData, loadFees, loadCountries } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
import {
|
||||
HostPortCell,
|
||||
NetTypeCell,
|
||||
ProtocolCell,
|
||||
CountryCellWithAsn,
|
||||
StatusCell,
|
||||
UptimeCell,
|
||||
EstimateFeeCell
|
||||
} from '$lib/components/datatables/xmr';
|
||||
|
||||
export let data;
|
||||
let filterNettype = 'any';
|
||||
let filterProtocol = 'any';
|
||||
let filterCc = 'any';
|
||||
let filterStatus = -1;
|
||||
let checkboxCors = false;
|
||||
|
||||
/** @type {{total_nodes: number, cc: string, name: string}[]} */
|
||||
let countries = [];
|
||||
let fees = [];
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
/** @type {Object.<string, number>} */
|
||||
let majorityFee;
|
||||
|
||||
onMount(() => {
|
||||
loadFees().then((data) => {
|
||||
fees = data;
|
||||
majorityFee = fees.reduce(
|
||||
/**
|
||||
* @param {Object.<string, number>} o
|
||||
* @param {{ nettype: string, estimate_fee: number }} key
|
||||
* @returns {Object.<string, number>}
|
||||
*/
|
||||
(o, key) => ({
|
||||
...o,
|
||||
[key.nettype]: key.estimate_fee
|
||||
}),
|
||||
{}
|
||||
);
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
loadCountries().then((data) => {
|
||||
countries = data;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<!-- prettier-ignore -->
|
||||
<p class="mx-auto max-w-3xl">
|
||||
<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 class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<section id="introduction ">
|
||||
<div class="section-container text-center !max-w-4xl">
|
||||
<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 always run your own node</strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="monero-remote-node">
|
||||
<div class="section-container">
|
||||
<div class="space-y-2 overflow-x-auto">
|
||||
<div class="flex justify-between">
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host:Port</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>
|
||||
<tr>
|
||||
<DtSrThFilter {handler} filterBy="host" placeholder="Filter Host / IP" />
|
||||
<th>
|
||||
<select
|
||||
id="nettype"
|
||||
name="nettype"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterNettype}
|
||||
on:change={() => {
|
||||
handler.filter(filterNettype, 'nettype');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
<option value="mainnet">MAINNET</option>
|
||||
<option value="stagenet">STAGENET</option>
|
||||
<option value="testnet">TESTNET</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select
|
||||
id="protocol"
|
||||
name="protocol"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterProtocol}
|
||||
on:change={() => {
|
||||
handler.filter(filterProtocol, 'protocol');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
<option value="tor">TOR</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select
|
||||
id="cc"
|
||||
name="cc"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterCc}
|
||||
on:change={() => {
|
||||
handler.filter(filterCc, 'cc');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
{#each countries as country}
|
||||
{#if country.cc === ''}
|
||||
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
|
||||
{:else}
|
||||
<option value={country.cc}
|
||||
>{country.name === '' ? country.cc : country.name} ({country.total_nodes})</option
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterStatus}
|
||||
on:change={() => {
|
||||
handler.filter(filterStatus, 'status');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="0">Offline</option>
|
||||
<option value="1">Online</option>
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
<label for="cors" class="flex items-center justify-center space-x-2">
|
||||
<input
|
||||
id="cors"
|
||||
name="cors"
|
||||
class="checkbox"
|
||||
type="checkbox"
|
||||
bind:checked={checkboxCors}
|
||||
on:change={() => {
|
||||
handler.filter(checkboxCors === true ? 1 : -1, 'cors');
|
||||
handler.invalidate();
|
||||
}}
|
||||
/>
|
||||
<p>CORS</p>
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td
|
||||
><HostPortCell
|
||||
ip={row.ip}
|
||||
is_tor={row.is_tor}
|
||||
hostname={row.hostname}
|
||||
port={row.port}
|
||||
/>
|
||||
<a class="anchor" href="/remote-nodes/logs/?node_id={row.id}">[Logs]</a>
|
||||
</td>
|
||||
<td><NetTypeCell nettype={row.nettype} height={row.height} /></td>
|
||||
<td><ProtocolCell protocol={row.protocol} cors={row.cors} /></td>
|
||||
<td
|
||||
><CountryCellWithAsn
|
||||
cc={row.cc}
|
||||
country_name={row.country_name}
|
||||
city={row.city}
|
||||
asn={row.asn}
|
||||
asn_name={row.asn_name}
|
||||
/></td
|
||||
>
|
||||
<td
|
||||
><StatusCell
|
||||
is_available={row.is_available}
|
||||
statuses={row.last_check_statuses}
|
||||
/></td
|
||||
>
|
||||
<td>
|
||||
<EstimateFeeCell
|
||||
estimate_fee={row.estimate_fee}
|
||||
majority_fee={majorityFee[row.nettype]}
|
||||
/>
|
||||
</td>
|
||||
<td><UptimeCell uptime={row.uptime} /></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 mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="page-info" class="mx-auto w-full max-w-4xl px-4 pb-7">
|
||||
<div class="alert card shadow-xl">
|
||||
<div class="alert-message">
|
||||
<h2 class="h3">Info</h2>
|
||||
<ul class="list-inside list-disc">
|
||||
<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-rose-900 font-bold">get_fee_estimate</code> RPC call method.
|
||||
</li>
|
||||
<li>
|
||||
Malicious actors who running remote nodes <a
|
||||
class="link"
|
||||
href="/img/node-tx-fee.jpg"
|
||||
rel="noopener">still can return high fee only if you about to create a transactions</a
|
||||
>.
|
||||
</li>
|
||||
<li><strong>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 know one or more remote node that we don't currently monitor, please add them using <a
|
||||
href="/add-node">this form</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
I deliberately cut the long Tor addresses, click the <span
|
||||
class="text-orange-800 dark:text-orange-300">👁 torhostname...</span
|
||||
> to see the full Tor address.
|
||||
</li>
|
||||
<li>
|
||||
You can found larger remote nodes database from <a
|
||||
class="external"
|
||||
href="https://monero.fail/"
|
||||
role="button"
|
||||
target="_blank"
|
||||
rel="noopener">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
|
||||
class="external"
|
||||
href="https://insights.ditatompel.com/en/blog/2022/01/public-api-monero-remote-node-list/"
|
||||
>Public API Monero Remote Node List</a
|
||||
> blog post for more detailed information.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,37 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
/**
|
||||
* @typedef {import('@vincjo/datatables/remote').State} State
|
||||
* @param {State} state - The state object from the data table.
|
||||
*/
|
||||
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 ?? [];
|
||||
};
|
||||
|
||||
export const loadCountries = async () => {
|
||||
const response = await fetch(apiUri('/api/v1/countries'));
|
||||
const json = await response.json();
|
||||
return json.data ?? [];
|
||||
};
|
||||
|
||||
export const loadFees = async () => {
|
||||
const response = await fetch(apiUri('/api/v1/fees'));
|
||||
const json = await response.json();
|
||||
return json.data ?? [];
|
||||
};
|
||||
|
||||
/** @param {State} state - The state object from the data table. */
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
/* prettier-ignore */
|
||||
return {
|
||||
meta: {
|
||||
title: 'Probe Logs',
|
||||
description: 'Monero RPC response frpm monitored remote nodes',
|
||||
keywords: 'monero log,monero node log,monitoring monero log,monero,xmr,monero node,xmrnode,cryptocurrency'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData, loadNodeInfo } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import { formatHashes, formatBytes } from '$lib/utils/strings';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
|
||||
/** @param {number | null } runtime */
|
||||
function parseRuntime(runtime) {
|
||||
return runtime === null ? '' : runtime.toLocaleString(undefined) + 's';
|
||||
}
|
||||
|
||||
export let data;
|
||||
|
||||
let pageId = '0';
|
||||
let filterStatus = -1;
|
||||
|
||||
/** @type {MoneroNode | null} */
|
||||
let nodeInfo;
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
onMount(() => {
|
||||
pageId = new URLSearchParams(window.location.search).get('node_id') || '0';
|
||||
loadNodeInfo(pageId).then((data) => {
|
||||
nodeInfo = data;
|
||||
});
|
||||
handler.filter(pageId, 'node_id');
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="card text-token mx-auto flex w-fit justify-center p-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="crumb"><a class="link underline" href="/remote-nodes">Remote Nodes</a></li>
|
||||
<li class="crumb-separator" aria-hidden>/</li>
|
||||
<li>Logs</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if nodeInfo === undefined}
|
||||
<div class="section-container mx-auto w-full max-w-3xl text-center">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else if nodeInfo === null}
|
||||
<div class="section-container mx-auto w-full max-w-3xl text-center">
|
||||
<p>Node ID does not exist</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="section-container">
|
||||
<div class="table-container mx-auto w-full max-w-3xl">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-bold">Hostname:Port</td>
|
||||
<td>{nodeInfo?.hostname}:{nodeInfo?.port}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Public IP</td>
|
||||
<td>{nodeInfo?.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Net Type</td>
|
||||
<td>{nodeInfo?.nettype.toUpperCase()}</td>
|
||||
</tr></tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="node-logs">
|
||||
<div class="section-container">
|
||||
<div class="space-y-2 overflow-x-auto">
|
||||
<div class="flex justify-between">
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#ID</th>
|
||||
<th>Prober ID</th>
|
||||
<th><label for="status">Status</label></th>
|
||||
<th>Height</th>
|
||||
<th>Adjusted Time</th>
|
||||
<th>DB Size</th>
|
||||
<th>Difficulty</th>
|
||||
<DtSrThSort {handler} orderBy="estimate_fee">Est. Fee</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="date_checked">Date Checked</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="fetch_runtime">Runtime</DtSrThSort>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterStatus}
|
||||
on:change={() => {
|
||||
handler.filter(filterStatus, 'status');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="1">Online</option>
|
||||
<option value="0">Offline</option>
|
||||
</select>
|
||||
</th>
|
||||
<DtSrThFilter
|
||||
{handler}
|
||||
filterBy="failed_reason"
|
||||
placeholder="Filter reason"
|
||||
colspan={7}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td>{row.id}</td>
|
||||
<td>{row.prober_id}</td>
|
||||
<td>{row.status === 1 ? 'OK' : 'ERR'}</td>
|
||||
{#if row.status !== 1}
|
||||
<td colspan="5">{row.failed_reason ?? ''}</td>
|
||||
{:else}
|
||||
<td class="text-right">{row.height.toLocaleString(undefined)}</td>
|
||||
<td>{format(row.adjusted_time * 1000, 'yyyy-MM-dd HH:mm')}</td>
|
||||
<td class="text-right">{formatBytes(row.database_size, 2)}</td>
|
||||
<td class="text-right">{formatHashes(row.difficulty)}</td>
|
||||
<td class="text-right">{row.estimate_fee.toLocaleString(undefined)}</td>
|
||||
{/if}
|
||||
<td>
|
||||
{format(row.date_checked * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.date_checked * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
<td class="text-right">{parseRuntime(row.fetch_runtime)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
|
@ -1,32 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
/**
|
||||
* @typedef {import('@vincjo/datatables/remote').State} State
|
||||
* @param {State} state - The state object from the data table.
|
||||
*/
|
||||
export const loadData = async (state) => {
|
||||
const response = await fetch(apiUri(`/api/v1/nodes/logs?${getParams(state)}`));
|
||||
const json = await response.json();
|
||||
state.setTotalRows(json.data.total_rows ?? 0);
|
||||
return json.data.items ?? [];
|
||||
};
|
||||
|
||||
/** @param {string} nodeId */
|
||||
export const loadNodeInfo = async (nodeId) => {
|
||||
const response = await fetch(apiUri(`/api/v1/nodes/id/${nodeId}`));
|
||||
const json = await response.json();
|
||||
return json.data;
|
||||
};
|
||||
|
||||
/** @param {State} state - The state object from the data table. */
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
Before ![]() (image error) Size: 1.8 KiB |
Before ![]() (image error) Size: 5.3 KiB |
Before ![]() (image error) Size: 5.7 KiB |
Before ![]() (image error) Size: 1.9 KiB |
Before ![]() (image error) Size: 2.1 KiB |
Before ![]() (image error) Size: 5.9 KiB |
Before ![]() (image error) Size: 2.7 KiB |
Before ![]() (image error) Size: 3.6 KiB |
Before ![]() (image error) Size: 6.7 KiB |
Before ![]() (image error) Size: 1.8 KiB |
Before ![]() (image error) Size: 19 KiB |
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"name": "xmr.ditatompel.com",
|
||||
"short_name": "xmr-remote-nodes",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#272b31",
|
||||
"theme_color": "#272b31",
|
||||
"description": "Monero Remote Nodes",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icon/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "4.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "4.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Allow: /
|
|
@ -1,58 +0,0 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import * as child_process from 'node:child_process';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Helper function to execute shell commands
|
||||
function execSync(cmd) {
|
||||
return child_process.execSync(cmd).toString().trim();
|
||||
}
|
||||
|
||||
// Read version from package.json
|
||||
const packageJsonPath = join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
const VERSION = packageJson.version;
|
||||
|
||||
// Retrieve current branch
|
||||
const BRANCH = execSync('git rev-parse --abbrev-ref HEAD');
|
||||
|
||||
// Retrieve current tag if it exists
|
||||
const RELEASE_TAG = execSync('git tag -l --points-at HEAD');
|
||||
|
||||
// Generate version suffix
|
||||
const commitCount = execSync('git rev-list --count HEAD');
|
||||
const shortCommitHash = execSync('git show --no-patch --no-notes --pretty="%h" HEAD');
|
||||
const VERSION_SUFFIX = `-beta.${commitCount}.${shortCommitHash}`;
|
||||
|
||||
// Determine branch-specific values
|
||||
let TAG_BRANCH = `.${BRANCH}`;
|
||||
|
||||
if (BRANCH === 'HEAD' || BRANCH === 'main') {
|
||||
TAG_BRANCH = '';
|
||||
}
|
||||
|
||||
// Determine final tag
|
||||
let TAG = `${VERSION}${VERSION_SUFFIX}${TAG_BRANCH}`;
|
||||
if (RELEASE_TAG) {
|
||||
TAG = RELEASE_TAG;
|
||||
}
|
||||
|
||||
console.log('Building with tag', TAG);
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
version: {
|
||||
name: TAG
|
||||
},
|
||||
// paths: {
|
||||
// base: '/'
|
||||
// },
|
||||
// trailingSlash: 'always',
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,23 +0,0 @@
|
|||
import { join } from 'path';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: ['skeleton']
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
// @ts-check
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
24
go.mod
|
@ -1,31 +1,35 @@
|
|||
module github.com/ditatompel/xmr-remote-nodes
|
||||
|
||||
go 1.22.2
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/a-h/templ v0.3.857
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/google/go-querystring v1.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/oschwald/geoip2-golang v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
golang.org/x/net v0.27.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
golang.org/x/net v0.39.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
|
|
45
go.sum
|
@ -1,14 +1,22 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
|
||||
github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
@ -17,8 +25,8 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
@ -26,8 +34,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||
|
@ -39,10 +47,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
|
@ -51,12 +59,13 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S
|
|||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|