Compare commits

...

81 commits

Author SHA1 Message Date
76c6a5514d
chore: Start v0.2.1 2024-11-15 17:45:46 +07:00
56b445d0aa
chore: Moving local vars from templ file to vars.go 2024-11-15 16:36:16 +07:00
ecfee86b2c
style: Changed hr color to orange 2024-11-15 16:15:28 +07:00
ce02334669
style: Added box shadow to navbar for better visibility 2024-11-15 15:52:06 +07:00
ditatombot[bot]
8c48e4749b
Merge pull request #159 from ditatompel/dependabot/go_modules/golang.org/x/net-0.31.0
Merge pull request #159

This merge action was created automatically.

Reviewed-by: ditatompel <ditatompel@users.noreply.github.com>
2024-11-15 08:15:27 +00:00
dependabot[bot]
eb6d709bbb
build(deps): bump golang.org/x/net from 0.30.0 to 0.31.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 05:35:58 +00:00
114078d3a1
fix(style): Unreadable navbar link color on light desktop 2024-11-08 17:36:19 +07:00
8e79d20b29
chore: Added a-h/templ link
Also mark "Accept I2P nodes" in ToDo's list as complete.
2024-11-08 17:17:31 +07:00
64da0beff9
style: Added badge for tor and i2p network #148 2024-11-08 03:21:42 +07:00
5e2ab83295
feat: Accept i2p naming service hostname #148
Please note that this naming service validation only validates simple
length and allowed characters. Advanced validation such as
internationalized domain name (IDN) is not implemented.

To minimize abuse, I also set minimum length of submitted i2p naming
service address to 5 characters. If someone have an address of 4
characters or less, let them open an issue or create a pull request.
2024-11-08 00:23:07 +07:00
e0313bdbe2
test: Added validI2PHostname unit test #148
Todo: Validate new format and allow naming service hostnames
2024-11-07 21:35:45 +07:00
f339bc9c3c
feat: Added remote nodes i2p filter #148 2024-11-07 20:52:38 +07:00
e892733a55
feat: Added i2p support #148
For now, only p32 address is supported.

ToDo: Accept i2p naming service from addressbook subscriptions
ToDo: Imporve i2p UI display and add i2p filter
2024-11-07 20:26:49 +07:00
15804ee438
chore: Update readme: Still show old license
Also mark migrating from Stelte to Templ + HTMX done in ToDo's section
2024-11-07 04:40:59 +07:00
060b3a3827
fix: Use meta link rel="icon" for favicon
Fiber's favicon middleware doesn't work with embed media
2024-11-07 01:57:57 +07:00
2e361a9fab
fix: Invalid reload button hx-get URL #155 2024-11-07 01:30:25 +07:00
98dcdfa94a
feat: Do not push query strings to URL #155
Slightly increase user browsing privacy by not pushing query strings
to browser URL. By using this method, the browser history stay on the
main page and filter query strings not recorded.

Note: This approach is experimental. Only tested on Firefox and Chromium
 browser.
2024-11-07 01:20:50 +07:00
c1c72274cf
feat: Added robots.txt route 2024-11-07 00:59:57 +07:00
5a22a0b71f
chore: Do not display nettype and IP addresses if empty 2024-11-07 00:53:53 +07:00
d60dbd86be
fix: Default remote nodes table sort by last_checked 2024-11-07 00:53:01 +07:00
e66f5bb1b8
chore(style): Changed main homepage buttons color to orange 2024-11-07 00:50:49 +07:00
b68f626ce2
refactor!: Use function method for routes
Will be useful for future development using standard `net/http`.
2024-11-06 22:15:53 +07:00
2e31824910
fix!: Redirect old /remote-nodes/logs to /remote-nodes/id/{id} #155
The old `/remote-nodes/logs/?node_id={id}` is not being used anymore
and should be redirected to the new path: `/remote-nodes/id/{id}`.

Remove the route once search engines result shows the new path
2024-11-06 21:34:05 +07:00
5fb88865d0
test: Added test for validTorHostname #149 2024-11-06 20:52:09 +07:00
f227371fa6
fix: Allow tor address with subdomain #149 2024-11-06 20:47:34 +07:00
3f5c2b9905
feat: Added TOR address validation #149 2024-11-06 20:21:15 +07:00
df161f831a
feat: Added info block in remote-nodes page
Also move the table right after page title and description, so users
doesn't need to scroll down to view the table.
2024-11-06 20:03:28 +07:00
75e97b4e0c
style: Styling remote-nodes hero hr divider 2024-11-06 19:35:31 +07:00
9e1da3c79a
style: Added link css class to internal URL 2024-11-06 19:32:34 +07:00
0f011572f5
chore: Updated the Monero Node block info detail 2024-11-06 19:17:10 +07:00
3beb3ba60e
feat: Added permalink header 2024-11-06 18:00:25 +07:00
fb6f6c2b5c
feat: Convert DatabaseSize and Difficulty to human readable format 2024-11-06 17:34:41 +07:00
1eb26210f6
refactor: Moving internal/views/utils.go to ./utils 2024-11-06 17:11:16 +07:00
95b371a056
feat! Added monero node details page and logs 2024-11-06 16:45:34 +07:00
fdf541f78f
chore(style): Align right estimate fee cell 2024-11-05 16:40:14 +07:00
a8c94ca0aa
feat!: Added Add node form and action 2024-11-04 23:53:09 +07:00
0acf12a277
fix: div #modal-section not inside html <body> tag 2024-11-04 23:52:00 +07:00
7da5fdb10c
chore: Change css class thead .th-filter to .frameless 2024-11-04 23:49:06 +07:00
1cd1b1a9c6
chore: Make hero gradient as component 2024-11-04 18:02:24 +07:00
e5eb23997b
feat: Added reload button for datatable 2024-11-04 17:29:07 +07:00
335f87b6d5
feat: Added auto refresh interval select options 2024-11-04 17:17:23 +07:00
721d1e8d6b
feat: Added modal 2024-11-04 16:36:22 +07:00
9cebe9d12f
feat: Added datatable sort functionality 2024-11-03 20:57:56 +07:00
6e7eccc6b3
chore: Stick with old SortDirection 2024-11-03 20:43:43 +07:00
f0a10208e2
feat!: Added CORS filter
DEPRECATED: Using int value for CORS is deprecated, please use "on" to
filter CORS capable nodes. Leave CORS empty to disable CORS filter.
2024-11-03 20:24:55 +07:00
c3b6f587ed
feat: Added filter by status 2024-11-03 18:03:13 +07:00
efc86d66fd
feat: Added filter by country 2024-11-03 17:37:11 +07:00
0165f0c251
feat: Added folter by protocol 2024-11-03 16:33:50 +07:00
e524c2686d
feat!: Remove old frontend codes 2024-11-03 16:33:15 +07:00
204865e50d
chore(ci): Temporary add htmx branch to test workflow 2024-11-03 16:26:52 +07:00
97f6312ce9
feat: Added table filter: host and nettype 2024-11-03 16:15:01 +07:00
ea0e0df57d
feat: Using relative time for last check nodes 2024-11-01 23:03:10 +07:00
c3c18ced05
feat: Added uptime cell component 2024-11-01 22:31:34 +07:00
44722f6b43
feat: Added node statuses cell component 2024-11-01 21:19:23 +07:00
babe61258a
feat: Added country cell to remote nodes table 2024-11-01 20:16:01 +07:00
7b5287fe9a
chore: Moving country flags location 2024-11-01 20:14:35 +07:00
f2cc795dc2
feat: Added protocol cell to remote nodes table 2024-11-01 19:50:56 +07:00
b23b0ae31a
feat: Added hostname:port cell to remote node table
TODO: Add modal window for tor addresses
2024-11-01 04:13:52 +07:00
751bfbc585
feat: Added nettype cell table 2024-11-01 03:05:29 +07:00
6efa763e73
style: Styling base datatable CSS 2024-10-31 23:08:05 +07:00
10182d9dbc
feat!: Added base datatable functionality
Deprecated: `SortDirection` is deprecated, use `SortDir` instead
2024-10-31 22:45:26 +07:00
ca3ca881fd
feat: Added paging package
Helper package for datatable pagination
2024-10-31 22:44:20 +07:00
ec6f0a1893
Changed LastChecked from uint to int64
Since the LastChecked record is storing unix timestamp, using `int64`
make it easier to work with `time` package.
2024-10-31 22:40:38 +07:00
30aa8d80dc
feat: Added favicon 2024-10-31 18:55:31 +07:00
f6adb40b3f
chore: Open GitHub repo in the new tab 2024-10-31 18:20:53 +07:00
93fb22f29b
feat: Added clipboard functionality 2024-10-31 18:09:02 +07:00
4da9c484a5
chore: Remove unused tailwind plugin import 2024-10-31 18:07:32 +07:00
63e803ba17
style: Added remote-nodes and add-node page design 2024-10-31 16:28:51 +07:00
ddc837be4a
style: Changed sticky navbar to fixed position 2024-10-31 16:26:49 +07:00
4dfab11d2c
feat(style)!: Added the new homepage design view
TODO: Add copy to clipboard functionality
2024-10-31 16:25:16 +07:00
0a80a52d2d
chore: Moving Monero QR donation image location 2024-10-31 16:21:43 +07:00
176a02412a
Switching to BSD-3-Clause license
I've been suggested to change the license to more popular open-source
licenses. So I choose to change from GLWTPL to BSD-3-Clause.
2024-10-31 10:59:08 +07:00
8b39502d90
chore(style): Changed UI layout styles 2024-10-31 10:39:18 +07:00
965d3230a1
feat: Added navbar current page position indicator 2024-10-30 15:39:01 +07:00
dd48bd458a
feat: Added global loading indicator 2024-10-30 15:10:35 +07:00
2003c3c3ac
feat(ui): Added main navbar 2024-10-30 14:23:45 +07:00
ca0af5848c
feat!: use specific required preline's plugin instead the whole js plugins 2024-10-30 14:13:17 +07:00
3a45071cd6
feat: Refactor fiber server 2024-10-29 21:30:04 +07:00
35d53c8a58
feat!: Update GitHub actions to work with templ and bun 2024-10-29 21:03:42 +07:00
be32011cfa
feat!: templ + htmx build system 2024-10-29 20:41:22 +07:00
03570a2200
chore: Start v0.2.0 (HTMX) 2024-10-29 20:25:03 +07:00
387 changed files with 5406 additions and 6530 deletions

View file

@ -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"

View file

@ -8,10 +8,13 @@ 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
# Fiber Config
APP_PREFORK=false

View file

@ -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

View file

@ -14,25 +14,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: 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
- name: Setup templ
run: go install github.com/a-h/templ/cmd/templ@v0.2.778
- name: Prepare assets
run: make prepare
- name: Cache Go modules
uses: actions/cache@v3
with:

View file

@ -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
- name: Setup templ
run: go install github.com/a-h/templ/cmd/templ@v0.2.778
# 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

View file

@ -2,6 +2,9 @@ on:
push:
branches:
- main
- htmx
- i2p-support
pull_request:
name: Test
jobs:
@ -12,11 +15,17 @@ 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
- name: Setup templ
run: go install github.com/a-h/templ/cmd/templ@v0.2.778
- name: Cache Go modules
uses: actions/cache@v3
with:
@ -25,22 +34,8 @@ jobs:
restore-keys: |
${{ runner.os }}-go-
- 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: Prepare assets
run: make prepare templ tailwind
- name: Run lint
uses: golangci/golangci-lint-action@v4

2
.gitignore vendored
View file

@ -3,3 +3,5 @@
/node_modules
/tmp
/assets/geoip
/internal/handler/views/assets/css/**/*
/internal/handler/views/assets/js/**/*

43
LICENSE
View file

@ -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.

View file

@ -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

View file

@ -28,7 +28,13 @@ serves the `/api` endpoint that is used by the clients and the Web UI itself.
To build the executable binaries, you need:
- Go >= 1.22
- NodeJS >= 20
- Bun >= 1.1.26
- [a-h/templ][templ-repo] v0.2.778
> **Note**:
>
> - If you want to contribute to the code, please use exact templ version
> (v0.2.778).
### Server & Prober requirements
@ -67,13 +73,18 @@ Systemd example: [xmr-nodes-prober.service][prober-systemd-service] and
## 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.
- Use `a-h/templ` and `HTMX` instead of `Svelte`.
- :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.
## Acknowledgement
@ -101,18 +112,20 @@ XMR Donation address:
8BWYe6GzbNKbxe3D8mPkfFMQA2rViaZJFhWShhZTjJCNG6EZHkXRZCKHiuKmwwe4DXDYF8KKcbGkvNYaiRG3sNt7JhnVp7D
```
![](./frontend/static/img/monerotip.png)
![](./internal/handler/views/assets/img/monerotip.png)
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/geoip2/geolite2/ "GeoIP 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"

View file

@ -1 +1 @@
v0.1.3
v0.2.1

BIN
bun.lockb Executable file

Binary file not shown.

View file

@ -21,11 +21,12 @@ import (
"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")
)
@ -41,6 +42,8 @@ type proberClient struct {
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
}
@ -52,6 +55,8 @@ func newProber() *proberClient {
apiKey: cfg.APIKey,
acceptTor: cfg.AcceptTor,
torSOCKS: cfg.TorSOCKS,
acceptI2P: cfg.AcceptI2P,
I2PSOCKS: cfg.I2PSOCKS,
acceptIPv6: cfg.IPv6Capable,
}
}
@ -67,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) {
@ -88,6 +96,10 @@ 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
}
@ -122,6 +134,9 @@ func (p *proberClient) validateConfig() error {
if p.acceptTor && p.torSOCKS == "" {
return errNoTorSocks
}
if p.acceptI2P && p.I2PSOCKS == "" {
return errNoI2PSocks
}
return nil
}
@ -133,6 +148,11 @@ func (p *proberClient) fetchJob() (monero.Node, error) {
acceptTor = 1
}
acceptI2P := 0
if p.acceptI2P {
acceptI2P = 1
}
acceptIPv6 := 0
if p.acceptIPv6 {
acceptIPv6 = 1
@ -140,7 +160,7 @@ func (p *proberClient) fetchJob() (monero.Node, error) {
var node monero.Node
uri := fmt.Sprintf("%s/api/v1/job?accept_tor=%d&accept_ipv6=%d", p.endpoint, acceptTor, acceptIPv6)
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)
@ -198,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
}
@ -268,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)
@ -335,7 +363,7 @@ func (p *proberClient) fetchFee(client http.Client, endpoint string) (uint, erro
}
func (p *proberClient) reportResult(node monero.Node, tookTime float64) error {
if !node.IsTor {
if !node.IsTor && !node.IsI2P {
if hostIps, err := net.LookupIP(node.Hostname); err == nil {
node.IPv6Only = ip.IsIPv6Only(hostIps)
node.IPAddresses = ip.SliceToString(hostIps)

View file

@ -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() {

View file

@ -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() {

10
frontend/.gitignore vendored
View file

@ -1,10 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -1 +0,0 @@
engine-strict=true

View file

@ -1,4 +0,0 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

@ -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).

View file

@ -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)
}

View file

@ -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/']
}
];

View file

@ -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
}

File diff suppressed because it is too large Load diff

View file

@ -1,39 +0,0 @@
{
"name": "xmr-nodes-frontend",
"version": "v0.1.3",
"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.11",
"@skeletonlabs/skeleton": "^2.10.3",
"@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.7.3",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/forms": "^0.5.9",
"@types/eslint": "^9.6.1",
"@vincjo/datatables": "^1.14.10",
"autoprefixer": "^10.4.20",
"date-fns": "^4.1.0",
"eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.1.3",
"svelte-check": "^4.0.5",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite": "^5.4.8"
},
"type": "module"
}

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View file

@ -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%);
}

34
frontend/src/app.d.ts vendored
View file

@ -1,34 +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;
ip_addresses: string;
}
interface ApiResponse {
status: string;
message: string;
data: null | object | object[];
}
}
export {};

View file

@ -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>

View file

@ -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>

View file

@ -1,11 +0,0 @@
<!-- prettier-ignore -->
<section id="site-news" class="mx-auto w-full max-w-4xl px-4 pb-7">
<div class="alert variant-ghost-secondary shadow-xl">
<div class="alert-message">
<h2 class="h3 text-center">Quick Info</h2>
<p>
On November 7th, 2024, localmonero.co website will be taken down. If you have an account and a balance, withdraw your funds prior to this date. For more information, visit <a href="https://localmonero.co/blog/announcements/winding-down" class="external" target="_blank" rel="noopener">localmonero.co announcement page</a>.
</p>
</div>
</div>
</section>

View file

@ -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>

View file

@ -1,66 +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')}> &#10094; </button>
<button class="mx-4">page <b>{$pageNumber}</b></button>
<button type="button" class="sm-btn" on:click={() => setPage('next')}>&#10095;</button>
{:else}
<div class="lg:hidden">
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> &#10094; </button>
<button class="mx-4">page <b>{$pageNumber}</b></button>
<button
class="sm-btn"
class:disabled={$pageNumber === $pageCount}
on:click={() => setPage('next')}
>
&#10095;
</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')}>&#10094;</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')}
>
&#10095;
</button>
</div>
{/if}
</section>
<style lang="postcss">
.disabled {
@apply cursor-not-allowed;
}
</style>

View file

@ -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></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}

View file

@ -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>

View file

@ -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}
/>

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -1,33 +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 !text-purple-800 dark:!text-purple-400"
href="https://www.ditatompel.com/asn/{asn}"
target="_blank"
rel="noopener">AS{asn}</a
>
(<span class="font-semibold text-green-800 dark:text-green-500">{asn_name}</span>)
{/if}
<style lang="postcss">
a {
@apply font-semibold text-sky-800 underline dark:text-sky-500;
}
</style>

View file

@ -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}

View file

@ -1,56 +0,0 @@
<script>
import { getModalStore } from '@skeletonlabs/skeleton';
import { formatHostname } from '$lib/utils/strings';
const modalStore = getModalStore();
/**
* @type {{
* is_tor: boolean,
* hostname: string,
* port: number,
* ipv6_only: boolean
* }}
*/
export let is_tor;
export let hostname;
export let port;
export let ipv6_only;
/** @type {string} */
export let ip_addresses;
/**
* @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-40 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}
{formatHostname(hostname)}:<span class="text-indigo-800 dark:text-indigo-400">{port}</span><br />
<div class="max-w-40 text-ellipsis overflow-x-auto md:overflow-hidden hover:overflow-visible">
<span class="whitespace-break-spaces text-gray-700 dark:text-gray-400"
>{ip_addresses.replace(/,/g, ' ')}</span
>
{#if ipv6_only}
<span class="text-rose-800 dark:text-rose-400">(IPv6 only)</span>
{/if}
</div>
{/if}

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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}

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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/' }
];

View file

@ -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

Width:  |  Height:  |  Size: 582 B

View file

@ -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

Width:  |  Height:  |  Size: 880 B

View file

@ -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

Width:  |  Height:  |  Size: 528 B

View file

@ -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

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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

Width:  |  Height:  |  Size: 831 B

View file

@ -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

Width:  |  Height:  |  Size: 470 B

View file

@ -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';

View file

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -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;
};

View file

@ -1,4 +0,0 @@
/** @param {string} path */
export const apiUri = (path) => {
return `${import.meta.env.VITE_API_URL || ''}${path}`;
};

View file

@ -1,76 +0,0 @@
/**
* Modifies the input string based on whether it is an IPv6 address.
* If the input is an IPv6 address, it wraps it in square brackets `[ ]`.
* Otherwise, it returns the input string as-is (for domain names or
* IPv4 addresses). AND I'M SORRY USING REGEX FOR THIS!
*
* @param {string} hostname
* @returns {string} - The modified string, IPv6 addresses wrapped in `[ ]`.
*/
export const formatHostname = (hostname) => {
// const ipv6Pattern = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/; // full
// pattern for both full and compressed IPv6 addresses.
// source: https://regex101.com/library/cP9mH9?filterFlavors=dotnet&filterFlavors=javascript&orderBy=RELEVANCE&search=ip
// This may be incorrect, but let's assume it's correct. xD
const ipv6Pattern =
/^(([0-9A-Fa-f]{1,4}:){7})([0-9A-Fa-f]{1,4})$|(([0-9A-Fa-f]{1,4}:){1,6}:)(([0-9A-Fa-f]{1,4}:){0,4})([0-9A-Fa-f]{1,4})$/;
if (ipv6Pattern.test(hostname)) {
return `[${hostname}]`;
}
return hostname;
};
/**
* @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';
};

View file

@ -1,2 +0,0 @@
export const prerender = true;
export const trailingSlash = 'always';

View file

@ -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 />

View file

@ -1,37 +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, but YOU BETTER RUN AND USE YOUR OWN NODE.',
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: 'revuo-xmr.com', uri: 'https://revuo-xmr.com' },
{ text: 'themonoeromoon.com', uri: 'https://www.themoneromoon.com' },
{ text: 'monerotopia.com', uri: 'https://monerotopia.com' },
{ text: 'sethforprivacy.com', uri: 'https://sethforprivacy.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'
}
};
}

View file

@ -1,187 +0,0 @@
<script>
import { clipboard } from '@skeletonlabs/skeleton';
import {
IcnGitHub,
IcnMonero,
IcnReddit,
IcnTwitter,
IcnFacebook,
IcnTelegram
} from '$lib/components/svg';
import News from '$lib/components/News.svelte';
/** @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>,&nbsp;
{/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 submitted Monero remote nodes you can use when you <strong>cannot</strong> 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 to be monitored and see how it performs.</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>
<News />
<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>

View file

@ -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'
}
};
}

View file

@ -1,129 +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="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 text-center">Important Note</h2>
<ul class="list-inside list-disc">
<li>
As an administrator of this instance, I have full rights to delete, and blacklist any
submitted node with or without providing any reason.
</li>
</ul>
</div>
</div>
</section>
<section id="form-add-monero-node">
<div class="section-container text-center">
<p>Enter your Monero node information below (IPv6 host check is experimental):</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 or 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>

View file

@ -1,11 +0,0 @@
/** @type {import('./$types').PageLoad} */
export async function load() {
return {
// prettier-ignore
meta: {
title: 'Public Monero Remote Nodes List',
description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!",
keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc'
}
};
}

View file

@ -1,343 +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';
import News from '$lib/components/News.svelte';
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 <span class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
</div>
</section>
<News />
<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_addresses={row.ip_addresses}
is_tor={row.is_tor}
hostname={row.hostname}
port={row.port}
ipv6_only={row.ipv6_only}
/>
</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} /><br />
<a
class="anchor !text-purple-800 dark:!text-purple-400"
href="/remote-nodes/logs/?node_id={row.id}">[Logs]</a
>
</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>
If you find any remote nodes that are strange or suspicious, please <a
class="external"
href="https://github.com/ditatompel/xmr-remote-nodes/issues"
target="_blank"
rel="noopener">open an issue on GitHub</a
> for removal.
</li>
<li>
Uptime percentage calculated is the <strong>last 1 month</strong> uptime.
</li>
<li>
<strong>Est. Fee</strong> here is just fee estimation / byte from
<code class="code text-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
class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500"
>The best and safest way is running your own node!</strong
>
</li>
<li>
Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You
can always add your node again latter.
</li>
<li>
You can filter remote node by selecting on <strong>nettype</strong>,
<strong>protocol</strong>, <strong>country</strong>,
<strong>tor</strong>, and <strong>online status</strong> option.
</li>
<li>
If you 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>

View file

@ -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;
};

View file

@ -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'
}
};
}

View file

@ -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 { formatHostname, 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="true">/</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>{formatHostname(nodeInfo?.hostname)}:{nodeInfo?.port}</td>
</tr>
<tr>
<td class="font-bold">Public IP</td>
<td>{nodeInfo?.ip_addresses.replace(/,/g, ', ')}</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}

View file

@ -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;
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -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"
}
]
}

View file

@ -1,2 +0,0 @@
User-agent: *
Allow: /

View file

@ -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;

View file

@ -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']
}
})
]
};

View file

@ -1,7 +0,0 @@
// @ts-check
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

8
go.mod
View file

@ -3,19 +3,21 @@ module github.com/ditatompel/xmr-remote-nodes
go 1.22.2
require (
github.com/a-h/templ v0.2.778
github.com/go-sql-driver/mysql v1.8.1
github.com/gofiber/fiber/v2 v2.52.5
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.30.0
golang.org/x/net v0.31.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/mattn/go-colorable v0.1.13 // indirect
@ -27,5 +29,5 @@ require (
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.26.0 // indirect
golang.org/x/sys v0.27.0 // indirect
)

20
go.sum
View file

@ -1,7 +1,9 @@
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/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
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.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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=
@ -9,6 +11,11 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
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/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=
@ -51,12 +58,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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=

View file

@ -13,6 +13,9 @@ type App struct {
LogLevel string
// configuration for server
URL string // URL where user can access the web UI, don't put trailing slash
// fiber specific config
Prefork bool
Host string
Port int
@ -24,6 +27,8 @@ type App struct {
APIKey string
AcceptTor bool
TorSOCKS string
AcceptI2P bool
I2PSOCKS string
IPv6Capable bool
}
@ -55,6 +60,9 @@ func LoadApp() {
}
// server configuration
app.URL = os.Getenv("APP_URL")
// fiber specific config
app.Host = os.Getenv("APP_HOST")
app.Port, _ = strconv.Atoi(os.Getenv("APP_PORT"))
app.Prefork, _ = strconv.ParseBool(os.Getenv("APP_PREFORK"))
@ -66,5 +74,7 @@ func LoadApp() {
app.APIKey = os.Getenv("API_KEY")
app.AcceptTor, _ = strconv.ParseBool(os.Getenv("ACCEPT_TOR"))
app.TorSOCKS = os.Getenv("TOR_SOCKS")
app.AcceptI2P, _ = strconv.ParseBool(os.Getenv("ACCEPT_I2P"))
app.I2PSOCKS = os.Getenv("I2P_SOCKS")
app.IPv6Capable, _ = strconv.ParseBool(os.Getenv("IPV6_CAPABLE"))
}

View file

@ -7,7 +7,7 @@ import (
type migrateFn func(*DB) error
var dbMigrate = [...]migrateFn{v1, v2, v3}
var dbMigrate = [...]migrateFn{v1, v2, v3, v4}
func MigrateDb(db *DB) error {
version := getSchemaVersion(db)
@ -272,3 +272,18 @@ func v3(db *DB) error {
return nil
}
func v4(db *DB) error {
slog.Debug("[DB] Migrating database schema version 4")
// table: tbl_node
slog.Debug("[DB] Adding additional columns to tbl_node")
_, err := db.Exec(`
ALTER TABLE tbl_node
ADD COLUMN is_i2p TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER is_tor;`)
if err != nil {
return err
}
return nil
}

Some files were not shown because too many files have changed in this diff Show more