148 Commits
Author SHA1 Message Date
WantClue 1c0b2d93e3 Merge pull request #27 from WantClue/patch-1
Update splash.component.scss
2025-11-28 19:14:41 +01:00
WantClue 6df8e78fa0 adding nerdeko nerdoct 2025-11-28 19:13:25 +01:00
WantClue 00954f4686 Update user-agent-link.component.html (#28)
add NerdAxe Gamma
2025-03-10 17:55:11 -04:00
WantClue 7840c7c0ec Update splash.component.scss
allow better visual stratum url on smaller screens
2025-02-24 19:37:42 +01:00
WantClue 703a43a817 Adding NerdNOS/NerdQAxe+/++ (#24)
* adding NerdNOS

* NerdQaxe+/++

* formatting
2025-02-01 19:22:53 -05:00
Benjamin Wilson 0794f97a50 number suffix 2025-02-01 15:33:42 -05:00
Benjamin Wilson e34fcfafa4 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2025-02-01 15:32:23 -05:00
Benjamin Wilson 1edec8e222 hash suffix 2025-02-01 15:32:20 -05:00
Seth For Privacy c2c7f2d3b7 Fix Docker builds and improve Dockerfile (#22)
* Fix Docker builds and improve Dockerfile

* Use latest bookworm-slim image for Node

* Properly name final image

* Use LTS Node version instead of latest
2024-11-25 11:27:53 -05:00
Benjamin Wilson 80081e337d add year calculation to other graphs 2024-08-24 21:17:03 -04:00
Benjamin Wilson c3149e2965 more precision 2024-08-22 10:46:10 -04:00
Benjamin Wilson 74f9d2e8c4 average time to block calc 2024-08-22 10:31:37 -04:00
Benjamin Wilson 3f7603ec99 logarithmic chart 2024-08-08 21:15:03 -04:00
WantClue 903665c07e adding NerdAxe (#16)
* adding NerdAxe

* formatting back
2024-08-08 16:58:53 -04:00
Benjamin Wilson c4533b6c2b ico 2024-07-11 20:52:51 -04:00
Benjamin Wilson 3943946575 png logo 2024-07-11 19:21:23 -04:00
Benjamin Wilson 1fe730c4d2 electron 2024-07-11 13:43:55 -04:00
Benjamin Wilson 11884281cb package updates 2024-07-10 15:40:13 -04:00
Thomas Shufps 3cb9de9710 added 0xaxe and qaxe+ (#14) 2024-05-01 17:45:38 -04:00
Ben 88680e15c6 align suffix with industry standard 2024-03-11 20:43:38 -04:00
Aaron Dewes ea4572c691 Clean up container (#13)
* Allow any domain

* Change log message
2024-02-17 09:52:27 -05:00
Ben Wilson 27de381f5e rank 2024-02-16 23:58:30 -05:00
Ben 483acc8858 bestDifficultyUserAgent 2024-02-16 19:06:11 -05:00
Ben Wilson fda08fb4dd highscore useragent 2024-02-16 17:44:16 -05:00
Ben Wilson 182d1e8eff bestDifficultyUserAgent 2024-02-16 17:40:39 -05:00
Ben Wilson 10d354a550 ago 2024-02-16 17:40:02 -05:00
Ben Wilson 97169aecea Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2024-02-16 17:22:20 -05:00
Ben Wilson 06be5d3299 highscores 2024-02-16 17:22:15 -05:00
Ben 14d6a60fb0 user agent 2024-02-13 23:20:34 -05:00
WantClue 069a3a2af2 Update user-agent-link.component.html (#11)
Add QAxe
Correct PiAxe linking to correct repo now
2024-01-26 09:12:09 -05:00
Jeremy Chabernaud d8f796354d misc: add details (#10)
Signed-off-by: djerfy <djerfy@gmail.com>
2024-01-14 09:24:00 -05:00
Matteo Crippa 38b01c6dcc Add support for LeafMiner (#9)
* add leafminer

* add FOSS star
2024-01-04 10:34:44 -05:00
Benjamin Wilson bea5e018b1 Merge pull request #7 from sethforprivacy/github_actions
Add Github Actions support
2023-12-13 12:00:54 -05:00
sethforprivacy 79139193e5 Initial Github Actions push
Fix environment file

Revert main.yml change
2023-12-13 11:33:03 -05:00
Ben Wilson 913ff61c83 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-12-12 21:39:52 -05:00
Ben Wilson 4f9e150252 add stratum rul to environment 2023-12-12 21:39:47 -05:00
Ben d989bb8921 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-12-10 10:29:05 -05:00
Ben 19a15c030d 2 hour chart 2023-12-10 10:29:01 -05:00
Ben Wilson 463fc9b676 PiAxe 2023-12-04 08:58:42 -05:00
Ben 00a2871c50 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-11-26 22:52:48 -05:00
Ben bb6196aab7 no fees 2023-11-26 22:52:15 -05:00
Ben Wilson 36a88b0fd3 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-11-07 13:10:11 -05:00
Ben Wilson 3b38c73950 redeploy 2023-11-07 13:10:06 -05:00
Benjamin Wilson 3c2018889f Merge pull request #4 from djerfy/fix/donate-align
fix: donate align
2023-08-19 15:53:23 -04:00
djerfy fb29d6a42e fix: donate align 2023-08-19 21:38:28 +02:00
Benjamin Wilson fd0fdac309 Merge pull request #3 from djerfy/feat/docker
feat: docker configuration
Thank you!
2023-08-19 12:43:41 -04:00
djerfy a563522ec0 feat: docker configuration 2023-08-19 18:35:03 +02:00
Ben Wilson 41ae76c10e cgminer 2023-08-09 21:31:14 -04:00
Ben Wilson f8438bfaac css, links 2023-08-09 21:24:44 -04:00
Ben Wilson 5027b8c600 favicon 2023-08-09 20:59:59 -04:00
Ben Wilson a93776f907 css work 2023-08-09 20:45:42 -04:00
Ben Wilson 4a4ece124f adding some loading indicators 2023-08-09 17:25:36 -04:00
Ben Wilson 690458b270 https 2023-08-09 11:35:47 -04:00
Ben Wilson 1df45d04a1 http 2023-08-09 11:14:34 -04:00
Ben Wilson 4f60ccfda6 https 2023-08-09 10:23:18 -04:00
Ben Wilson 4e9e61fd28 cname 2 2023-08-08 22:39:23 -04:00
Ben Wilson 63b64005e9 cname 2023-08-08 22:34:48 -04:00
Ben Wilson 6e644b03fa use public-pool.io 2023-08-08 22:27:47 -04:00
Ben Wilson 789fc6064e github build 2023-08-08 22:13:08 -04:00
Ben Wilson 6c604c46a2 test? 2023-08-08 22:05:57 -04:00
Ben Wilson 472a632b96 expand table by default 2023-08-08 00:20:12 -04:00
Ben Wilson ec2c2a955c rows now showing 2023-08-06 19:16:10 -04:00
Ben Wilson 4a980a9176 sum hashrate 2023-08-06 18:16:58 -04:00
Ben Wilson ad9f831b91 last seen 2023-08-06 18:00:03 -04:00
Ben Wilson 1516f90712 fixed chart sizing 2023-08-06 17:43:14 -04:00
Ben Wilson 09ba050849 uptime 2023-08-05 23:03:22 -04:00
Ben Wilson 7106c997b6 spelling 2023-08-04 16:50:52 -04:00
Ben Wilson d8241362e6 network stats 2023-08-03 22:41:50 -04:00
Ben Wilson b32b2ef3ab https api 2023-08-03 17:58:58 -04:00
Ben Wilson 5b0be8a652 test gzipper for gh 2023-08-03 17:53:54 -04:00
Ben Wilson db98e8c6b7 gh pages 2023-08-03 17:48:04 -04:00
Ben Wilson d5b172e25f build 2023-08-03 17:36:40 -04:00
Ben Wilson 6e73ea28ea github pages 2023-08-03 17:34:28 -04:00
Ben Wilson 16373774d3 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-08-03 16:47:25 -04:00
Ben Wilson 22bc9ed266 package optimizations 2023-08-03 16:47:01 -04:00
Ben 79f85f1187 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-08-03 16:44:27 -04:00
Ben 6a865b0827 proxy.prod 2023-08-03 16:43:55 -04:00
Ben Wilson aac2cc219b gzip 2023-08-03 12:15:15 -04:00
Ben Wilson 4437dd4c63 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-08-01 22:38:28 -04:00
Ben Wilson 9db2938cf1 data rework 2023-08-01 22:38:24 -04:00
Benjamin Wilson 78fba9bada Merge pull request #2 from djerfy/fix/worker-total-hash-rate
fix: worker total hash rate (issue #1)
2023-08-01 15:21:20 -04:00
djerfy f907d99d8d fix: worker total hash rate (issue #1) 2023-08-01 21:14:25 +02:00
Ben Wilson a6af5ff82e text 2023-07-31 23:04:29 -04:00
Ben Wilson 00a6e2cc63 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-07-31 23:01:17 -04:00
Ben Wilson 8ffbd09bd6 chart 2023-07-31 23:01:14 -04:00
Ben 8c51c86ff1 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-07-26 14:52:12 -04:00
Ben da257201b2 splash input 2023-07-26 14:52:04 -04:00
Ben Wilson c34b618a9a useragent 2023-07-26 09:43:30 -04:00
Ben Wilson 920304be99 style 2023-07-25 20:51:17 -04:00
Ben Wilson 86c1da13d3 bug 2023-07-25 20:49:14 -04:00
Ben Wilson b0f4573485 user agent pipe 2023-07-25 20:46:57 -04:00
Ben Wilson 405b5d3cca user agents 2023-07-25 18:53:59 -04:00
Ben Wilson a57741de61 icons 2023-07-25 09:36:43 -04:00
Ben Wilson b6d86d0566 input switch 2023-07-24 22:31:46 -04:00
Ben Wilson 898141a8fe particles setting 2023-07-24 22:20:55 -04:00
Ben Wilson 1cfb2dbb25 best diff on dashboard 2023-07-23 00:15:42 -04:00
Ben Wilson feb00e9f5a tofixed 2023-07-22 18:45:47 -04:00
Ben Wilson fe42b01021 nerdminer fix 2023-07-22 18:40:01 -04:00
Ben Wilson aad653bf0b build 2023-07-21 22:55:35 -04:00
Ben Wilson 82cbb38ea6 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-07-21 22:50:03 -04:00
Ben Wilson 6b78772be7 nerdminers 2023-07-21 22:50:00 -04:00
Benjamin Wilson 8b8458832b Update README.md 2023-07-17 00:25:45 -04:00
Benjamin Wilson 6ff1b356d3 Update README.md 2023-07-17 00:20:49 -04:00
Ben Wilson 429d60dda8 info and colors 2023-07-16 19:33:41 -04:00
Ben Wilson bcecb28dc1 date 2023-07-14 09:36:03 -04:00
Ben Wilson 3aaab06db0 date 2023-07-14 09:31:52 -04:00
Ben Wilson 4a04b896dd dark mode 2023-07-13 19:26:34 -04:00
Ben Wilson a485de4b5f found blocks services and tables 2023-07-12 22:53:09 -04:00
Ben Wilson b359f24dbe HashSuffixPipe 2023-07-11 17:38:00 -04:00
Ben Wilson a6623a539e correct hashrate suffix 2023-07-10 21:11:25 -04:00
Ben Wilson 217eb83985 Merge branch 'master' of https://github.com/benjamin-wilson/public-pool-ui 2023-07-10 20:14:25 -04:00
Ben Wilson 1768964e0f splash chart 2023-07-10 20:14:21 -04:00
Ben 1c00ab899a wrong stratum port 2023-07-09 11:01:07 -04:00
Ben c10e4b2951 build size 2023-07-02 19:31:45 -04:00
Ben 4841b3a65a ui work 2023-07-02 16:18:50 -04:00
Ben Wilson d06e52d6e5 time range updates 2023-06-28 07:30:58 -04:00
Ben Wilson 53164ac40d added some validation to splash 2023-06-26 18:19:38 -04:00
Ben Wilson b3302fb84c add 2hr chart 2023-06-25 16:35:29 -04:00
Ben Wilson 0c0ca21ac8 charts 2023-06-25 09:16:27 -04:00
Ben Wilson 3ba7ef8ca9 misc 2023-06-24 12:54:21 -04:00
Ben Wilson 48516e468e formatting 2023-06-22 09:31:24 -04:00
Ben Wilson b9337ae636 charting, routing and stats 2023-06-22 09:06:39 -04:00
Ben Wilson f547adb550 build 2023-06-21 16:27:06 -04:00
Ben Wilson d7f578aa50 delete workflows 2023-06-21 16:26:40 -04:00
Ben Wilson b672a5092b --configuration=production 2023-06-21 13:55:55 -04:00
Ben Wilson 6d4e30ddee env2 2023-06-21 13:55:23 -04:00
Ben Wilson 50a48c5dae env 2023-06-21 13:48:07 -04:00
Ben Wilson 86644cdb00 api url 2023-06-21 13:32:06 -04:00
Ben Wilson 83f757ce27 image cleanup 2023-06-21 11:35:43 -04:00
Ben Wilson 38ece7462e baseref 2023-06-21 10:47:34 -04:00
Ben Wilson 526dcd4ac7 dist 2023-06-21 10:35:10 -04:00
Ben Wilson c81ad70e88 use GITHUB_TOKEN 2023-06-21 10:20:27 -04:00
Ben Wilson 9691b4edb7 update token 2023-06-21 10:15:09 -04:00
Ben Wilson 99a2ad747b add pages branch 2023-06-21 10:04:40 -04:00
Ben Wilson 6345ce97c7 increase bundle size 2023-06-21 09:55:50 -04:00
Ben Wilson 29fe7a4fa5 Pages 2023-06-21 09:50:31 -04:00
Ben Wilson cf69e02156 pages 2023-06-21 09:49:38 -04:00
Ben Wilson 72ecfdff5c test 2023-06-21 09:45:43 -04:00
Ben Wilson 4dea0b828a pages 2023-06-21 09:44:46 -04:00
Ben Wilson 40f559fbe5 pages 2023-06-21 09:38:56 -04:00
Ben Wilson 0627fae904 pages 2023-06-21 09:36:38 -04:00
Ben Wilson ae127ccde2 github action 2023-06-21 09:31:30 -04:00
Ben Wilson 83d4e80d52 github action 2023-06-21 09:26:55 -04:00
Ben Wilson 42f6232e5f logo 2023-06-20 23:21:38 -04:00
Ben Wilson d9b18b920c ui init 2023-06-20 23:11:11 -04:00
Ben Wilson 9aafc19923 splash init 2023-06-20 22:50:02 -04:00
Ben Wilson 86fe6ca57b theme init 2023-06-20 21:58:55 -04:00
Ben Wilson edd312017a initial commit 2023-06-20 18:56:23 -04:00
256 changed files with 18233 additions and 1725 deletions
+4
View File
@@ -0,0 +1,4 @@
dist
node_modules
README.md
.vscode
+16
View File
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+35
View File
@@ -0,0 +1,35 @@
name: "Test build of image when Dockerfile is changed"
on:
push:
branches-ignore:
- master
pull_request:
workflow_dispatch:
jobs:
rebuild-container:
name: "Build image with cache"
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
with:
platforms: linux/arm64
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Build image
id: docker_build_amd64
uses: docker/build-push-action@v5.1.0
with:
push: false
load: true
platforms: linux/amd64
tags: public-pool-ui:amd64
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
+32
View File
@@ -0,0 +1,32 @@
name: GitHub Pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci --force
- name: Build
run: npm run build:github
- name: Deploy
if: success()
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist/public-pool-ui
enable_jekyll: true
+37
View File
@@ -0,0 +1,37 @@
name: Build and scan container for vulnerabilities with Trivy
on:
push:
pull_request:
schedule:
- cron: '22 14 * * 0'
workflow_dispatch:
jobs:
build:
name: Build and scan images
runs-on: ubuntu-latest
steps:
-
name: Checkout code
uses: actions/checkout@v4
-
name: Build image from Dockerfile
uses: docker/build-push-action@v5.1.0
with:
push: false
load: true
tags: ${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
-
name: Run Trivy vulnerability scanner against "latest" image
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
-
name: Upload "latest" Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
+61
View File
@@ -0,0 +1,61 @@
name: "Update image and push to Github Packages and Docker Hub weekly"
on:
schedule:
- cron: "0 12 * * 1" # Run every Monday at noon.
workflow_dispatch:
jobs:
rebuild-container:
name: "Rebuild Container with the latest base image"
runs-on: ubuntu-latest
steps:
-
name: Prepare outputs
id: prep
run: |
echo "::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
with:
platforms: linux/arm64
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Get short SHA
id: get_short_sha
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
name: Build and push to Docker Hub and GitHub Packages Docker Registry
id: docker_build
uses: docker/build-push-action@v5.1.0
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/public-pool-ui:latest
ghcr.io/${{ github.repository_owner }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
@@ -0,0 +1,64 @@
name: "Update image when Dockerfile is changed"
on:
push:
branches:
- master
workflow_dispatch:
jobs:
rebuild-container:
name: "Rebuild Container with the latest base image"
runs-on: ubuntu-latest
steps:
-
name: Prepare outputs
id: prep
run: |
echo "::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
with:
platforms: linux/arm64
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Get short SHA
id: get_short_sha
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
name: Build and push to Docker Hub and GitHub Packages Docker Registry
uses: docker/build-push-action@v5.1.0
id: docker_build_push
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/public-pool-ui:latest
ghcr.io/${{ github.repository_owner }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
cache-to: type=inline
+42
View File
@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
+4
View File
@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}
+20
View File
@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"url": "http://localhost:4200/",
"sourceMaps": true
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}
+42
View File
@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}
-1410
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
############################
# Docker build environment #
############################
FROM node:lts-bookworm-slim AS build
# Upgrade all packages and install dependencies
RUN apt-get update \
&& apt-get upgrade -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3 \
build-essential \
&& apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /build
COPY . .
# Build Public Pool UI using NPM
RUN npm i && npm run build
############################
# Docker final environment #
############################
FROM caddy:alpine AS final
EXPOSE 80
WORKDIR /var/www/html
COPY --from=build /build/dist/public-pool-ui .
COPY docker/Caddyfile.tpl /etc/Caddyfile.tpl
COPY docker/entrypoint.sh /entrypoint.sh
CMD ["/bin/sh", "/entrypoint.sh"]
+53
View File
@@ -0,0 +1,53 @@
# PublicPoolUi
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.3.
## Dependencies
Requires [Public-Pool](https://github.com/benjamin-wilson/public-pool) to be running
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
## Deployment
Install pm2 (https://pm2.keymetrics.io/)
```bash
$ pm2 serve --spa dist/public-pool-ui/ 3335 --name ui
```
## Docker
```bash
$ docker build -t public-pool-ui .
$ docker run --name public-pool-ui --rm -p 8080:80 public-pool-ui
```
From Docker commands, website will be accessible on [http://localhost:8080](http://localhost:8080). By default Caddy server listen on port 80, but we bind it to port 8080 which allows you to launch image without root permissions.
Available variables:
* `DOMAIN`: website domain (default: `localhost`)
* `LOGLEVEL`: loglevel in stdout (default: `INFO`)
* `LOGFORMAT`: log format in stdout (default: `json`)
+162
View File
@@ -0,0 +1,162 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"public-pool-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/public-pool-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles"
]
},
"scripts": [
"node_modules/chart.js/dist/chart.js"
]
},
"configurations": {
"electron": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.electron.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "public-pool-ui:build:production"
},
"development": {
"browserTarget": "public-pool-ui:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "public-pool-ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

+10
View File
@@ -0,0 +1,10 @@
:80 {
root * /var/www/html
file_server
log {
output stdout
format %%LOGFORMAT%%
level %%LOGLEVEL%%
}
}
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
if [ ! -e "/etc/Caddyfile" ]; then
sed -i "s#%%LOGLEVEL%%#${LOGLEVEL:-INFO}#g" /etc/Caddyfile.tpl
sed -i "s#%%LOGFORMAT%%#${LOGFORMAT:-json}#g" /etc/Caddyfile.tpl
mv /etc/Caddyfile.tpl /etc/Caddyfile
else
rm -f /etc/Caddyfile.tpl
fi
echo "Starting UI on port 80"
echo "Logs output: ${LOGLEVEL:-INFO} (${LOGFORMAT:-json})"
exec caddy run --config /etc/Caddyfile
Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 B

-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en" data-critters-container>
<head>
<meta charset="utf-8">
<title>Public Pool</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>:root{--surface-a:#1f2d40;--surface-b:#17212f;--surface-c:rgba(255, 255, 255, 0.03);--surface-d:#304562;--surface-e:#1f2d40;--surface-f:#1f2d40;--text-color:rgba(255, 255, 255, 0.87);--text-color-secondary:rgba(255, 255, 255, 0.6);--primary-color:#64B5F6;--primary-color-text:#212529;--font-family:-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--surface-0:#17212f;--surface-50:#2e3744;--surface-100:#454d59;--surface-200:#5d646d;--surface-300:#747a82;--surface-400:#8b9097;--surface-500:#a2a6ac;--surface-600:#b9bcc1;--surface-700:#d1d3d5;--surface-800:#e8e9ea;--surface-900:#ffffff;--gray-50:#e8e9ea;--gray-100:#d1d3d5;--gray-200:#b9bcc1;--gray-300:#a2a6ac;--gray-400:#8b9097;--gray-500:#747a82;--gray-600:#5d646d;--gray-700:#454d59;--gray-800:#2e3744;--gray-900:#17212f;--content-padding:1rem;--inline-spacing:0.5rem;--border-radius:3px;--surface-ground:#17212f;--surface-section:#17212f;--surface-card:#1f2d40;--surface-overlay:#1f2d40;--surface-border:#304562;--surface-hover:rgba(255,255,255,.03);--maskbg:rgba(0, 0, 0, 0.4);--focus-ring:0 0 0 1px #93cbf9;color-scheme:light dark}*{box-sizing:border-box}:root{--blue-50:#f4fafe;--blue-100:#cae6fc;--blue-200:#a0d2fa;--blue-300:#75bef8;--blue-400:#4baaf5;--blue-500:#2196f3;--blue-600:#1c80cf;--blue-700:#1769aa;--blue-800:#125386;--blue-900:#0d3c61;--green-50:#f6fbf6;--green-100:#d4ecd5;--green-200:#b2ddb4;--green-300:#90cd93;--green-400:#6ebe71;--green-500:#4caf50;--green-600:#419544;--green-700:#357b38;--green-800:#2a602c;--green-900:#1e4620;--yellow-50:#fffcf5;--yellow-100:#fef0cd;--yellow-200:#fde4a5;--yellow-300:#fdd87d;--yellow-400:#fccc55;--yellow-500:#fbc02d;--yellow-600:#d5a326;--yellow-700:#b08620;--yellow-800:#8a6a19;--yellow-900:#644d12;--cyan-50:#f2fcfd;--cyan-100:#c2eff5;--cyan-200:#91e2ed;--cyan-300:#61d5e4;--cyan-400:#30c9dc;--cyan-500:#00bcd4;--cyan-600:#00a0b4;--cyan-700:#008494;--cyan-800:#006775;--cyan-900:#004b55;--pink-50:#fef4f7;--pink-100:#fac9da;--pink-200:#f69ebc;--pink-300:#f1749e;--pink-400:#ed4981;--pink-500:#e91e63;--pink-600:#c61a54;--pink-700:#a31545;--pink-800:#801136;--pink-900:#5d0c28;--indigo-50:#f5f6fb;--indigo-100:#d1d5ed;--indigo-200:#acb4df;--indigo-300:#8893d1;--indigo-400:#6372c3;--indigo-500:#3f51b5;--indigo-600:#36459a;--indigo-700:#2c397f;--indigo-800:#232d64;--indigo-900:#192048;--teal-50:#f2faf9;--teal-100:#c2e6e2;--teal-200:#91d2cc;--teal-300:#61beb5;--teal-400:#30aa9f;--teal-500:#009688;--teal-600:#008074;--teal-700:#00695f;--teal-800:#00534b;--teal-900:#003c36;--orange-50:#fff8f2;--orange-100:#fde0c2;--orange-200:#fbc791;--orange-300:#f9ae61;--orange-400:#f79530;--orange-500:#f57c00;--orange-600:#d06900;--orange-700:#ac5700;--orange-800:#874400;--orange-900:#623200;--bluegray-50:#f7f9f9;--bluegray-100:#d9e0e3;--bluegray-200:#bbc7cd;--bluegray-300:#9caeb7;--bluegray-400:#7e96a1;--bluegray-500:#607d8b;--bluegray-600:#526a76;--bluegray-700:#435861;--bluegray-800:#35454c;--bluegray-900:#263238;--purple-50:#faf4fb;--purple-100:#e7cbec;--purple-200:#d4a2dd;--purple-300:#c279ce;--purple-400:#af50bf;--purple-500:#9c27b0;--purple-600:#852196;--purple-700:#6d1b7b;--purple-800:#561561;--purple-900:#3e1046;--red-50:#fff5f5;--red-100:#ffd1ce;--red-200:#ffada7;--red-300:#ff8980;--red-400:#ff6459;--red-500:#ff4032;--red-600:#d9362b;--red-700:#b32d23;--red-800:#8c231c;--red-900:#661a14;--primary-50:#f7fbff;--primary-100:#daedfd;--primary-200:#bcdffb;--primary-300:#9fd1f9;--primary-400:#81c3f8;--primary-500:#64b5f6;--primary-600:#559ad1;--primary-700:#467fac;--primary-800:#376487;--primary-900:#284862}</style><link id="theme-css" rel="stylesheet" type="text/css" href="assets/layout/styles/theme/vela-blue/theme.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="assets/layout/styles/theme/vela-blue/theme.css"></noscript>
<style>*{box-sizing:border-box}html{font-size:14px}body{font-family:var(--font-family);color:var(--text-color);background-color:var(--surface-ground);margin:0;padding:0;min-height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}</style><link rel="stylesheet" href="styles.86ee2b39234242ec.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.86ee2b39234242ec.css"></noscript></head>
<body>
<app-root></app-root>
<script src="runtime.69a46451081d025b.js" type="module"></script><script src="polyfills.315fefdcc45f5d18.js" type="module"></script><script src="scripts.ee09a5738baa913c.js" defer></script><script src="main.27fb8544590bf3bd.js" type="module"></script></body>
</html>
File diff suppressed because one or more lines are too long
+14576
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "public-pool-ui",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.config.local.json ",
"start:prod": "ng serve --configuration=production",
"build": "ng build --configuration=production && gzipper compress --gzip --brotli ./dist/public-pool-ui/",
"build:electron": "ng build --configuration=electron --base-href ./",
"build:github": "ng build --configuration=production && echo \"web.public-pool.io\" > dist/public-pool-ui/CNAME",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"bundle-report": "ng build --configuration=production --source-map=true --named-chunks=true --stats-json && webpack-bundle-analyzer dist/public-pool-ui/stats.json"
},
"private": true,
"dependencies": {
"@angular/animations": "18.1.0",
"@angular/common": "18.1.0",
"@angular/compiler": "18.1.0",
"@angular/core": "18.1.0",
"@angular/forms": "18.1.0",
"@angular/platform-browser": "18.1.0",
"@angular/platform-browser-dynamic": "18.1.0",
"@angular/router": "18.1.0",
"bitcoin-address-validation": "^2.2.3",
"chart.js": "^4.4.3",
"chartjs-adapter-moment": "^1.0.1",
"moment": "^2.30.1",
"ng-particles": "^3.12.0",
"ngx-device-detector": "8.0.0",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
"primeng": "17.18.3",
"rxjs": "~7.8.1",
"tslib": "^2.3.0",
"tsparticles": "^2.10.1",
"tsparticles-engine": "^2.10.1",
"zone.js": "~0.14.7"
},
"devDependencies": {
"@angular-devkit/build-angular": "18.1.0",
"@angular/cli": "18.1.0",
"@angular/compiler-cli": "^18.1.0",
"@types/jasmine": "~4.0.0",
"gzipper": "^7.2.0",
"jasmine-core": "~4.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~5.5.3",
"webpack-bundle-analyzer": "^4.9.0"
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
{
"/api": {
"target": "https://localhost:3334",
"secure": true,
"changeOrigin": false,
"logLevel": "debug"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"/api": {
"target": "https://public-pool.io:40557",
"secure": true,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": "/api"
}
}
}
-1
View File
@@ -1 +0,0 @@
(()=>{"use strict";var e,_={},p={};function a(e){var n=p[e];if(void 0!==n)return n.exports;var r=p[e]={id:e,loaded:!1,exports:{}};return _[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=_,e=[],a.O=(n,r,c,f)=>{if(!r){var o=1/0;for(l=0;l<e.length;l++){for(var[r,c,f]=e[l],s=!0,t=0;t<r.length;t++)(!1&f||o>=f)&&Object.keys(a.O).every(h=>a.O[h](r[t]))?r.splice(t--,1):(s=!1,f<o&&(o=f));if(s){e.splice(l--,1);var u=c();void 0!==u&&(n=u)}}return n}f=f||0;for(var l=e.length;l>0&&e[l-1][2]>f;l--)e[l]=e[l-1];e[l]=[r,c,f]},a.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return a.d(n,{a:n}),n},a.d=(e,n)=>{for(var r in n)a.o(n,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},a.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e={666:0};a.O.j=c=>0===e[c];var n=(c,f)=>{var t,u,[l,o,s]=f,i=0;if(l.some(d=>0!==e[d])){for(t in o)a.o(o,t)&&(a.m[t]=o[t]);if(s)var v=s(a)}for(c&&c(f);i<l.length;i++)a.o(e,u=l[i])&&e[u]&&e[u][0](),e[u]=0;return a.O(v)},r=self.webpackChunkpublic_pool_ui=self.webpackChunkpublic_pool_ui||[];r.forEach(n.bind(null,0)),r.push=n.bind(null,r.push.bind(r))})()})();
File diff suppressed because one or more lines are too long
+57
View File
@@ -0,0 +1,57 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { SettingsComponent } from './components/settings/settings.component';
import { SplashComponent } from './components/splash/splash.component';
import { WorkerGroupComponent } from './components/worker-group/worker-group.component';
import { WorkerComponent } from './components/worker/worker.component';
import { AppLayoutComponent } from './layout/app.layout.component';
const routes: Routes = [
{
path: '',
component: SplashComponent
},
{
path: 'app',
component: AppLayoutComponent,
children: [
{
path: ':address',
children: [
{
path: 'settings',
component: SettingsComponent
},
{
path: '',
component: DashboardComponent,
},
{
path: ':workerName',
children: [
{
path: '',
component: WorkerGroupComponent
},
{
path: ':workerId',
component: WorkerComponent
}
]
},
]
}
]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
+2
View File
@@ -0,0 +1,2 @@
<app-background-particles *ngIf="particles$ | async"></app-background-particles>
<router-outlet></router-outlet>
View File
+29
View File
@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent]
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'public-pool-ui'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('public-pool-ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('public-pool-ui app is running!');
});
});
+18
View File
@@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { LocalStorageService } from './services/local-storage.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'public-pool-ui';
public particles$: Observable<boolean>;
constructor(private localService: LocalStorageService) {
this.particles$ = this.localService.particles$;
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'chartjs-adapter-moment';
import { CommonModule, HashLocationStrategy, LocationStrategy } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgParticlesModule } from 'ng-particles';
import { PrimeNGModule } from '../prime-ng.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BackgroundParticlesComponent } from './components/background-particles/background-particles.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { SettingsComponent } from './components/settings/settings.component';
import { SplashComponent } from './components/splash/splash.component';
import { UserAgentLinkComponent } from './components/user-agent-link/user-agent-link.component';
import { WorkerGroupComponent } from './components/worker-group/worker-group.component';
import { WorkerComponent } from './components/worker/worker.component';
import { AppLayoutModule } from './layout/app.layout.module';
import { DateAgoPipe } from './pipes/date-ago.pipe';
import { HashSuffixPipe } from './pipes/hash-suffix.pipe';
import { NumberSuffixPipe } from './pipes/number-suffix.pipe';
import { AverageTimeToBlockPipe } from './pipes/average-time-to-block.pipe';
@NgModule({
declarations: [
AppComponent,
SplashComponent,
DashboardComponent,
WorkerComponent,
NumberSuffixPipe,
DateAgoPipe,
WorkerGroupComponent,
BackgroundParticlesComponent,
HashSuffixPipe,
SettingsComponent,
UserAgentLinkComponent
],
imports: [
CommonModule,
BrowserModule,
AppRoutingModule,
PrimeNGModule,
AppLayoutModule,
FormsModule,
ReactiveFormsModule,
NgParticlesModule
],
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy },
],
bootstrap: [AppComponent]
})
export class AppModule { }
@@ -0,0 +1,2 @@
<ng-particles id="particles" [options]="particleOptions" [style]="particleStyles" [particlesInit]="particlesInit"
(particlesLoaded)="particlesLoaded($event)"></ng-particles>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BackgroundParticlesComponent } from './background-particles.component';
describe('BackgroundParticlesComponent', () => {
let component: BackgroundParticlesComponent;
let fixture: ComponentFixture<BackgroundParticlesComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BackgroundParticlesComponent]
});
fixture = TestBed.createComponent(BackgroundParticlesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,89 @@
import { Component, OnInit } from '@angular/core';
import { IParticlesProps } from 'ng-particles';
import { DeviceDetectorService } from 'ngx-device-detector';
import { loadFull } from 'tsparticles';
import { Container, Engine } from 'tsparticles-engine';
@Component({
selector: 'app-background-particles',
templateUrl: './background-particles.component.html',
styleUrls: ['./background-particles.component.scss']
})
export class BackgroundParticlesComponent implements OnInit {
public particleStyles!: any;
public particleOptions!: IParticlesProps;
constructor(private deviceService: DeviceDetectorService) { }
ngOnInit(): void {
this.particleStyles = {
position: 'fixed',
width: '100%',
height: '100%',
'z-index': -1,
top: 0,
left: 0,
right: 0,
bottom: 0,
};
const color: string = '#96bcf9';
const lineColor: string = '#afccfa';
this.particleOptions = {
fpsLimit: 30,
detectRetina: true,
background: {
position: "50% 50%",
repeat: "no-repeat",
size: "cover"
},
fullScreen: {
zIndex: 1
},
particles: {
number: {
value: 80,
density: {
enable: true,
value_area: 800
}
},
color: {
value: color
},
opacity: { value: 0.6 },
links: {
distance: 150,
enable: true,
color: {
value: lineColor
},
opacity: 0.6
},
move: {
enable: true,
speed: 0.4
},
size: {
random: true,
}
}
}
}
particlesLoaded(container: Container): void {
}
async particlesInit(engine: Engine): Promise<void> {
// Starting from 1.19.0 you can add custom presets or shape here, using the current tsParticles instance (main)
// this loads the tsparticles package bundle, it's the easiest method for getting everything ready
// starting from v2 you can add only the features you need reducing the bundle size
await loadFull(engine);
}
}
@@ -0,0 +1,241 @@
<ng-container>
<div class="grid">
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-4">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Your Best Difficulty <i
class="pi pi-question-circle ml-2"
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i></span>
<div class="text-900 font-medium text-xl">
<ng-container
*ngIf="clientInfo$ | async as clientInfo; else loading">{{clientInfo.bestDifficulty |
numberSuffix}}</ng-container>
<ng-template #loading>
<p-skeleton></p-skeleton>
</ng-template>
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">
<ng-container *ngIf="clientInfo$ | async as clientInfo; else loading">{{clientInfo.bestDifficulty |
number}}</ng-container>
<ng-template #loading>
<p-skeleton></p-skeleton>
</ng-template>
</span>
</div>
</div>
<ng-container>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-4">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Network Difficulty</span>
<div class="text-900 font-medium text-xl">
<ng-container
*ngIf="networkInfo$ | async as networkInfo; else loading">{{networkInfo.difficulty |
numberSuffix}}</ng-container>
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
</div>
</div>
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div> -->
</div>
<span class="text-green-500 font-medium">
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
{{networkInfo.difficulty
| number}}</ng-container>
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-4">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Network Hash Rate </span>
<div class="text-900 font-medium text-xl">
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
{{networkInfo.networkhashps | hashSuffix}}</ng-container>
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
</div>
</div>
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div> -->
</div>
<span class="text-green-500 font-medium">&nbsp;</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-4">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Block Height</span>
<div class="text-900 font-medium text-xl">
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
{{networkInfo.blocks}}</ng-container>
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
</div>
</div>
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div> -->
</div>
<span class="text-green-500 font-medium">Weight:
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
{{networkInfo.currentblockweight | number}}</ng-container>
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
</span>
</div>
</div>
</ng-container>
</div>
<div class="card">
<ng-container *ngIf="clientInfo$ | async as clientInfo; else loadingTable">
<p-table #dataTable [rowHover]="true" groupRowsBy="name" dataKey="name" rowGroupMode="subheader"
[value]="clientInfo.workers" [expandedRowKeys]="expandedRows$ | async"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template pTemplate="header">
<tr>
<th>Name</th>
<th>Session ID</th>
<th>Hash Rate</th>
<th>Session Best Difficulty</th>
<th>Uptime</th>
<th>Last Seen</th>
</tr>
</ng-template>
<ng-template pTemplate="groupheader" let-worker let-rowIndex="rowIndex" let-expanded="expanded">
<tr [routerLink]="[worker.name]">
<td>
<button (click)="$event.stopImmediatePropagation()" type="button" pButton pRipple
[pRowToggler]="worker" class="p-button-text p-button-rounded p-button-plain mr-2"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
<span class="worker-name font-bold ml-2">{{worker.name}} </span>
</td>
<td>
{{getSessionCount(worker.name, clientInfo.workers)}} Sessions
</td>
<td>
{{getTotalHashRate(worker.name, clientInfo.workers) | hashSuffix}}
</td>
<td>
{{getBestDifficulty(worker.name, clientInfo.workers) | numberSuffix}}
</td>
<td>
</td>
<td></td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-worker>
<tr [routerLink]="[worker.name, worker.sessionId]">
<td></td>
<td>{{worker.sessionId}}</td>
<td>{{worker.hashRate | hashSuffix}}</td>
<td>{{worker.bestDifficulty | numberSuffix}}</td>
<td>{{worker.startTime | dateAgo}}</td>
<td>{{worker.lastSeen | dateAgo}}</td>
</tr>
</ng-template>
</p-table>
</ng-container>
<ng-template #loadingTable>
<p-table #dataTable [rowHover]="true" groupRowsBy="loading" dataKey="loading" rowGroupMode="subheader"
[value]="[{loading: 'loading'}]" [expandedRowKeys]="{loading: true}"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template pTemplate="header">
<tr>
<th>Name</th>
<th>Session ID</th>
<th>Hash Rate</th>
<th>Session Best Difficulty</th>
<th>Uptime</th>
<th>Last Seen</th>
</tr>
</ng-template>
<ng-template pTemplate="groupheader" let-worker let-rowIndex="rowIndex" let-expanded="expanded">
<tr>
<td>
<button (click)="$event.stopImmediatePropagation()" type="button" pButton pRipple
[pRowToggler]="worker" class="p-button-text p-button-rounded p-button-plain mr-2"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
<span class="worker-name font-bold ml-2">{{worker.name}} </span>
</td>
<td>
<p-skeleton></p-skeleton>
</td>
<td>
<p-skeleton></p-skeleton>
<td>
<p-skeleton></p-skeleton>
</td>
<td>
</td>
<td></td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-worker>
<tr>
<td></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
</tr>
</ng-template>
</p-table>
</ng-template>
</div>
<div class="card chart">
<ng-container *ngIf="chartData$ | async as chartData; else loadingChart">
<p-chart [data]="chartData" [options]="chartOptions"></p-chart>
</ng-container>
<ng-template #loadingChart>
<p-skeleton width="100%" height="40vh"></p-skeleton>
</ng-template>
</div>
</ng-container>
@@ -0,0 +1,3 @@
.worker-name {
line-height: 42px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,197 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Table } from 'primeng/table';
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
import { AppService } from '../../services/app.service';
import { ClientService } from '../../services/client.service';
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements AfterViewInit {
public address: string;
public clientInfo$: Observable<any>;
public chartData$: Observable<any>;
public chartOptions: any;
public networkInfo$: Observable<any>;
private networkInfo:any;
@ViewChild('dataTable') dataTable!: Table;
public expandedRows$: Observable<any>;
constructor(
private clientService: ClientService,
private route: ActivatedRoute,
private appService: AppService
) {
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
this.address = this.route.snapshot.params['address'];
this.clientInfo$ = this.clientService.getClientInfo(this.address).pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
this.expandedRows$ = this.clientInfo$.pipe(map((info: any) => {
return info.workers.reduce((pre: any, cur: any) => { pre[cur.name] = true; return pre; }, {});
}));
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
this.chartData$ = combineLatest([this.clientService.getClientInfoChart(this.address), this.networkInfo$]).pipe(
map(([chartData, networkInfo]) => {
this.networkInfo = networkInfo;
const GROUP_SIZE = 12; //6 = 1 hour
let hourlyData = [];
for (let i = GROUP_SIZE; i < chartData.length; i += GROUP_SIZE) {
let sum = 0;
for (let j = GROUP_SIZE - 1; j >= 0; j--) {
sum += parseInt(chartData[i - j].data);
}
sum = sum / GROUP_SIZE;
hourlyData.push({ y: sum, x: chartData[i].label });
}
const data = chartData.map((d: any) => { return { y: d.data, x: d.label } });
return {
labels: chartData.map((d: any) => d.label),
datasets: [
{
type: 'line',
label: '2 Hour',
data: hourlyData,
fill: false,
backgroundColor: documentStyle.getPropertyValue('--yellow-600'),
borderColor: documentStyle.getPropertyValue('--yellow-600'),
tension: .4,
pointRadius: 1,
borderWidth: 1
},
{
type: 'line',
label: '10 Minute',
data: data,
fill: false,
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
borderColor: documentStyle.getPropertyValue('--primary-color'),
tension: .4,
pointRadius: 1,
borderWidth: 1
},
]
}
})
);
this.chartOptions = {
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'hour', // Set the unit to 'minute'
},
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder,
drawBorder: false,
display: true
}
},
y: {
ticks: {
color: textColorSecondary,
callback: (value: number) => {
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
}
},
grid: {
color: surfaceBorder,
drawBorder: false
}
}
}
};
}
ngAfterViewInit() {
}
public getSessionCount(name: string, workers: any[]) {
const workersByName = workers.filter(w => w.name == name);
return workersByName.length;
}
public getTotalHashRate(name: string, workers: any[]) {
const workersByName = workers.filter(w => w.name == name);
const sum = workersByName.reduce((pre, cur, idx, arr) => {
return pre += Math.floor(cur.hashRate);
}, 0);
return Math.floor(sum);
}
public getBestDifficulty(name: string, workers: any[]) {
const workersByName = workers.filter(w => w.name == name);
const best = workersByName.reduce((pre, cur, idx, arr) => {
if (cur.bestDifficulty > pre) {
return cur.bestDifficulty;
}
return pre;
}, 0);
return best;
}
public getTotalUptime(name: string, workers: any[]) {
const now = new Date().getTime();
const workersByName = workers.filter(w => w.name == name);
const sum = workersByName.reduce((pre, cur, idx, arr) => {
return pre += now - new Date(cur.startTime).getTime();
}, 0);
return new Date(now - sum);
}
}
@@ -0,0 +1,5 @@
<div class="card">
<h5>Background Particles </h5>
<p-inputSwitch [(ngModel)]="value" (ngModelChange)="particlesChanged($event)" optionLabel="label"
optionValue="value"></p-inputSwitch>
</div>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SettingsComponent]
});
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { LocalStorageService } from '../../services/local-storage.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent {
public stateOptions: any[] = [{ label: 'On', value: true }, { label: 'Off', value: false },];
public value: boolean = true;
constructor(private localStorageService: LocalStorageService) {
this.value = this.localStorageService.getParticles();
}
public particlesChanged(newVal: boolean) {
this.localStorageService.setParticles(newVal);
this.value = newVal;
}
}
@@ -0,0 +1,239 @@
<div class="py-4 px-1 md:px4 lg:px-8 mx-0 lg:mx-8 main">
<div class="grid justify-content-center">
<div class="col-12">
<div class="card text-center">
<div>
<img id="logo" style="height: 200px;" src="assets/layout/images/logo.svg" alt="logo">
</div>
<div class="col-12 text-center title">
<h1 class="text-900 font-normal">Public Pool</h1>
</div>
<div class="mb-4">
<a class="ml-4 mr-4" href="https://twitter.com/Public_Pool_BTC" target="_blank">
<i class="pi pi-twitter" style="font-size: 30pt; color: white;"></i>
</a>
<a class="ml-4 mr-4" href="https://discord.gg/pF9smpe3yE" target="_blank">
<i class="pi pi-discord" style="font-size: 30pt; color: white;"></i>
</a>
<a class="ml-4 mr-4" href="https://github.com/benjamin-wilson/public-pool" target="_blank">
<i class="pi pi-github" style="font-size: 30pt; color: white;"></i>
</a>
</div>
<div>
<code>Fully Open Source Solo Bitcoin Mining Pool</code>
<br>
<br>
<div class="info mb-2 p-4 border-dashed border-1 border-round">
<code>stratum+tcp://{{stratumURL}}</code>
<br>
<code>username: &lt;your BTC address&gt;.&lt;worker name&gt;, password: x</code>
</div>
<span><b>NO FEES</b></span>
<br>
<span>No second best.</span>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<h4>Login</h4>
<div class="field text-center">
<input [formControl]="address" placeholder="Address (bc1...)" pInputText id="address" type="text"
class="p-inputtext p-component p-element">
<button [disabled]="address.invalid" class="ml-3 mt-3" pButton label="My Workers"
[routerLink]="['app',address.value]"></button>
</div>
</div>
</div>
<div class="col-12">
<div class="card chart">
<div class="text-center mb-2">Uptime: {{uptime$ | async | dateAgo}}</div>
<ng-container *ngIf="chartData$ | async as chartData; else loadingChart">
<p-chart [responsive]="true" type="line" [data]="chartData" [options]="chartOptions"></p-chart>
</ng-container>
<ng-template #loadingChart>
<p-skeleton width="100%" height="40vh"></p-skeleton>
</ng-template>
</div>
</div>
<div class="col-12">
<div class="card">
<h4 style="text-align: center;">Online Devices</h4>
<ng-container *ngIf="userAgents$ | async as userAgents">
<p-table [value]="userAgents">
<ng-template pTemplate="header">
<tr>
<th>Device</th>
<th>Currently Working</th>
<th>Total Hash Rate</th>
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-userAgent>
<tr></tr>
<tr>
<td><app-user-agent-link [userAgent]="userAgent.userAgent"></app-user-agent-link></td>
<td>{{ userAgent.count }}</td>
<td>{{ userAgent.totalHashRate | hashSuffix }}</td>
<td>{{ userAgent.bestDifficulty | numberSuffix}}</td>
</tr>
</ng-template>
</p-table>
<div class="text-right mt-2">
<i class="pi pi-star-fill"></i> = Open Source Software & Hardware
</div>
</ng-container>
<ng-template #loadingTable>
<p-table [value]="[{},{},{}]">
<ng-template pTemplate="header">
<tr>
<th>Device</th>
<th>Currently Working</th>
<th>Total Hash Rate</th>
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr></tr>
<tr>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
</tr>
</ng-template>
</p-table>
</ng-template>
</div>
</div>
<div class="col-12" *ngIf="highScores$ | async as highScores">
<div class="card">
<h4 style="text-align: center;">High Scores</h4>
<ng-container>
<p-table [value]="highScores">
<ng-template pTemplate="header">
<tr>
<th>Rank</th>
<th>Difficulty</th>
<th>Device</th>
<th>When</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-highScore let-i="rowIndex">
<tr></tr>
<tr>
<td>#{{i+1}}</td>
<td>{{ highScore.bestDifficulty | numberSuffix }}</td>
<td><app-user-agent-link
[userAgent]="highScore.bestDifficultyUserAgent ? highScore.bestDifficultyUserAgent : 'unknown'"></app-user-agent-link>
</td>
<td>{{ highScore.updatedAt | dateAgo}} ago</td>
</tr>
</ng-template>
</p-table>
<div class="text-right mt-2">
<i class="pi pi-star-fill"></i> = Open Source Software & Hardware
</div>
</ng-container>
<ng-template #loadingTable>
<p-table [value]="[{},{},{}]">
<ng-template pTemplate="header">
<tr>
<th>Device</th>
<th>Currently Working</th>
<th>Total Hash Rate</th>
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr></tr>
<tr>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
<td><p-skeleton></p-skeleton></td>
</tr>
</ng-template>
</p-table>
</ng-template>
</div>
</div>
<div class="col-12" *ngIf="blockData$ | async as blockData">
<div class="card">
<h4 style="text-align: center;"> Found Blocks</h4>
<p-table [value]="blockData">
<ng-template pTemplate="header">
<tr>
<th>Height</th>
<th>Address</th>
<th>Worker</th>
<th>Session</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-block>
<tr>
<td>{{ block.height }}</td>
<td>{{ block.minerAddress }}</td>
<td>{{ block.worker }}</td>
<td>{{ block.sessionId }}</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="grid">
<div class="col-12 md:col-2">
<h3>Donate</h3>
<p>Like the project? Consider a donation. </p>
</div>
<div class="col-12 md:col-4 text-center">
<img style="height: 200px;" src="assets/layout/images/qr-code-ln.svg"
alt="bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d">
<div>public_pool&#64;strike.me</div>
</div>
<div class="col-12 md:col-6 text-center">
<img style="height: 200px;" src="assets/layout/images/qr-code-onchain.svg"
alt="bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d">
<div>bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,77 @@
#address {
min-width: 50%;
}
#logo {
left: 50%;
margin-left: -100px;
height: 200px;
width: 200px;
position: absolute;
top: -18px;
}
.title {
margin-top: 55px;
}
.main {
margin-top: 60px;
}
.info {
font-weight: bold;
font-size: x-large;
}
.info code {
display: inline-block;
white-space: nowrap;
font-size: inherit;
}
/* Desktop styles */
@media screen and (min-width: 769px) {
.info {
padding: 1rem;
text-align: center;
}
.info code {
max-width: 90%;
margin: 0 auto;
}
}
/* Tablet and mobile styles */
@media screen and (max-width: 768px) {
.info {
font-size: large;
padding: 0.5rem;
}
.info code {
white-space: pre-wrap;
word-wrap: break-word;
font-size: 0.9em;
}
}
/* Very small screens */
@media screen and (max-width: 480px) {
.info {
font-size: medium;
padding: 0.25rem;
}
}
.card {
max-width: 1024px;
margin: 0 auto;
}
.pi-star-fill {
color: gold;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SplashComponent } from './splash.component';
describe('SplashComponent', () => {
let component: SplashComponent;
let fixture: ComponentFixture<SplashComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SplashComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(SplashComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,123 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
import { environment } from '../../../environments/environment';
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
import { AppService } from '../../services/app.service';
import { bitcoinAddressValidator } from '../../validators/bitcoin-address.validator';
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
@Component({
selector: 'app-splash',
templateUrl: './splash.component.html',
styleUrls: ['./splash.component.scss']
})
export class SplashComponent {
public address: FormControl;
public chartData$: Observable<any>;
public blockData$: Observable<any>;
public userAgents$: Observable<any>;
public highScores$: Observable<any>;
public uptime$: Observable<string>;
public chartOptions: any;
public stratumURL = '';
private info$: Observable<any>;
private networkInfo:any;
constructor(private appService: AppService, private cdr: ChangeDetectorRef) {
this.info$ = this.appService.getInfo().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
if (environment.STRATUM_URL.length > 1) {
this.stratumURL = environment.STRATUM_URL;
} else {
this.stratumURL = window.location.hostname + ':3333';
}
this.blockData$ = this.info$.pipe(map(info => info.blockData));
this.userAgents$ = this.info$.pipe(map(info => info.userAgents));
this.highScores$ = this.info$.pipe(map(info => info.highScores));
this.uptime$ = this.info$.pipe(map(info => info.uptime))
this.chartData$ = combineLatest([this.appService.getInfoChart(), this.appService.getNetworkInfo()]).pipe(
map(([chartData, networkInfo]) => {
this.networkInfo = networkInfo;
return {
labels: chartData.map((d: any) => d.label),
datasets: [
{
label: 'Public-Pool Hashrate',
data: chartData.map((d: any) => d.data),
fill: false,
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
borderColor: documentStyle.getPropertyValue('--primary-color'),
tension: .4,
pointRadius: 1,
borderWidth: 1
}
]
}
})
);
this.address = new FormControl(null, bitcoinAddressValidator());
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
this.chartOptions = {
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day', // Set the unit to 'minute'
},
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder,
drawBorder: false,
display: true
}
},
y: {
ticks: {
color: textColorSecondary,
callback: (value: number) => {
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
}
},
grid: {
color: surfaceBorder,
drawBorder: false
},
type: 'logarithmic',
}
}
};
}
}
@@ -0,0 +1,95 @@
<ng-container [ngSwitch]="userAgent">
<ng-container *ngSwitchCase="'NerdMiner'">
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdMiner_v2" target="_blank">Nerd Miner
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdNOS'">
<a (click)="cancelClick($event)" href="https://github.com/WantClue/NerdMiner_v2/tree/nerdnos" target="_blank">NerdNOS
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdAxe'">
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdAxeUltra" target="_blank">NerdAxe
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdAxeGamma'">
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdAxe" target="_blank">NerdAxeGamma
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdOCTAXE'">
<a (click)="cancelClick($event)" href="https://github.com/Patsch91/NerdOCTAXE-Gamma" target="_blank">NerdOCTAXE
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdEKO'">
<a (click)="cancelClick($event)" href="https://github.com/phil31/NerdEKO-Gamma" target="_blank">NerdEKO
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'PiAxe'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/piaxe" target="_blank">PiAxe
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'QAxe'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">QAxe
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'QAxe+'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">QAxe+
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdQAxe+'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">NerdQAxe+
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'NerdQAxe++'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">NerdQAxe++
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'0xAxe'">
<a (click)="cancelClick($event)" href="https://github.com/shufps/0xaxe" target="_blank">0xAxe
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'bitaxe'">
<a (click)="cancelClick($event)" href="https://github.com/skot/bitaxe/" target="_blank">Bitaxe
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'bitaxeHex'">
<a (click)="cancelClick($event)" href="https://github.com/skot/bitaxeHex/" target="_blank">Bitaxe Hex
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchCase="'cpuminer'">
<a (click)="cancelClick($event)" href="https://github.com/pooler/cpuminer" target="_blank">cpuminer
</a>
</ng-container>
<ng-container *ngSwitchCase="'cgminer'">
<a (click)="cancelClick($event)" href="https://cgminer.info/" target="_blank">cgminer
</a>
</ng-container>
<ng-container *ngSwitchCase="'Braiins OS'">
<a (click)="cancelClick($event)" href="https://braiins.com/os/plus" target="_blank">Braiins OS
</a>
</ng-container>
<ng-container *ngSwitchCase="'termux-miner'">
<a (click)="cancelClick($event)" href="https://github.com/wong-fi-hung/termux-miner"
target="_blank">termux-miner
</a>
</ng-container>
<ng-container *ngSwitchCase="'LeafMiner'">
<a (click)="cancelClick($event)" href="https://github.com/matteocrippa/leafminer" target="_blank">LeafMiner
<i class="pi pi-star-fill"></i>
</a>
</ng-container>
<ng-container *ngSwitchDefault>{{userAgent}}</ng-container>
</ng-container>
@@ -0,0 +1,12 @@
.pi-external-link {
margin-left: 10px;
vertical-align: middle;
}
.pi-star-fill {
margin-left: 25px;
color: gold;
vertical-align: middle;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserAgentLinkComponent } from './user-agent-link.component';
describe('UserAgentLinkComponent', () => {
let component: UserAgentLinkComponent;
let fixture: ComponentFixture<UserAgentLinkComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserAgentLinkComponent]
});
fixture = TestBed.createComponent(UserAgentLinkComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-agent-link',
templateUrl: './user-agent-link.component.html',
styleUrls: ['./user-agent-link.component.scss']
})
export class UserAgentLinkComponent {
@Input('userAgent') userAgent!: string;
constructor() {
}
public cancelClick(event: Event) {
event.stopImmediatePropagation();
}
}
@@ -0,0 +1,75 @@
<div class="grid">
<ng-container *ngIf="workerInfo$ | async as workerInfo">
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">ID</span>
<div class="text-900 font-medium text-xl">{{workerInfo.name}}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-id-card text-blue-500 text-xl"></i>
</div>
</div>
<!-- <span class="text-green-500 font-medium">24 new </span> -->
<span class="text-500">&nbsp;</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Best Difficulty</span>
<div class="text-900 font-medium text-xl">{{workerInfo.bestDifficulty | numberSuffix}}
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">{{workerInfo.bestDifficulty | number}}</span>
</div>
</div>
<!-- <div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Uptime</span>
<div class="text-900 font-medium text-xl">{{workerInfo.startTime | dateAgo}}</div>
</div>
<div class="flex align-items-center justify-content-center bg-cyan-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-clock text-cyan-500 text-xl"></i>
</div>
</div>
<span class="text-500">Since {{workerInfo.startTime | date : 'short'}}</span>
</div>
</div> -->
<!-- <div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Comments</span>
<div class="text-900 font-medium text-xl">152 Unread</div>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-comment text-purple-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">85 </span>
<span class="text-500">responded</span>
</div>
</div> -->
</ng-container>
<div class="col-12" *ngIf="chartData$ | async as chartData">
<div class="card chart">
<p-chart type="line" [data]="chartData" [options]="chartOptions"></p-chart>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkerGroupComponent } from './worker-group.component';
describe('WorkerGroupComponent', () => {
let component: WorkerGroupComponent;
let fixture: ComponentFixture<WorkerGroupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WorkerGroupComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(WorkerGroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,109 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
import { WorkerService } from '../../services/worker.service';
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
import { AppService } from 'src/app/services/app.service';
@Component({
selector: 'app-worker-group',
templateUrl: './worker-group.component.html',
styleUrls: ['./worker-group.component.scss']
})
export class WorkerGroupComponent {
public workerInfo$: Observable<any>;
public chartData$: Observable<any>;
public chartOptions: any;
public networkInfo$: Observable<any>;
private networkInfo:any;
constructor(
private workerService: WorkerService,
private route: ActivatedRoute,
private appService: AppService
) {
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
this.workerInfo$ = this.workerService.getGroupWorkerInfo(this.route.snapshot.params['address'], this.route.snapshot.params['workerName']).pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
this.chartData$ = combineLatest([this.workerInfo$, this.networkInfo$]).pipe(
map(([workerInfo, networkInfo]) => {
this.networkInfo = networkInfo;
return {
labels: workerInfo.chartData.map((d: any) => d.label),
datasets: [
{
label: workerInfo.name,
data: workerInfo.chartData.map((d: any) => d.data),
fill: false,
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
borderColor: documentStyle.getPropertyValue('--primary-color'),
tension: .4,
pointRadius: 1,
borderWidth: 1
}
]
}
})
);
this.chartOptions = {
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day'
},
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder,
drawBorder: false
}
},
y: {
ticks: {
color: textColorSecondary,
callback: (value: number) => {
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
}
},
grid: {
color: surfaceBorder,
drawBorder: false
}
}
}
};
}
}
@@ -0,0 +1,75 @@
<div class="grid">
<ng-container *ngIf="workerInfo$ | async as workerInfo">
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">ID</span>
<div class="text-900 font-medium text-xl">{{workerInfo.name}}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-id-card text-blue-500 text-xl"></i>
</div>
</div>
<!-- <span class="text-green-500 font-medium">24 new </span> -->
<span class="text-500">{{workerInfo.sessionId}}</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Best Difficulty</span>
<div class="text-900 font-medium text-xl">{{workerInfo.bestDifficulty | numberSuffix}}
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">{{workerInfo.bestDifficulty | number}}</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Uptime</span>
<div class="text-900 font-medium text-xl">{{workerInfo.startTime | dateAgo}}</div>
</div>
<div class="flex align-items-center justify-content-center bg-cyan-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-clock text-cyan-500 text-xl"></i>
</div>
</div>
<!-- <span class="text-green-500 font-medium">520 </span>-->
<span class="text-500">Since {{workerInfo.startTime | date : 'short'}}</span>
</div>
</div>
<!-- <div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Comments</span>
<div class="text-900 font-medium text-xl">152 Unread</div>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-comment text-purple-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">85 </span>
<span class="text-500">responded</span>
</div>
</div> -->
</ng-container>
<div class="col-12" *ngIf="chartData$ | async as chartData">
<div class="card chart">
<p-chart type="line" [data]="chartData" [options]="chartOptions"></p-chart>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkerComponent } from './worker.component';
describe('WorkerComponent', () => {
let component: WorkerComponent;
let fixture: ComponentFixture<WorkerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WorkerComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(WorkerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,111 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
import { WorkerService } from '../../services/worker.service';
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
import { AppService } from 'src/app/services/app.service';
@Component({
selector: 'app-worker',
templateUrl: './worker.component.html',
styleUrls: ['./worker.component.scss']
})
export class WorkerComponent {
public workerInfo$: Observable<any>;
public chartData$: Observable<any>;
public chartOptions: any;
public networkInfo$: Observable<any>;
private networkInfo:any;
constructor(
private workerService: WorkerService,
private route: ActivatedRoute,
private appService: AppService
) {
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
this.workerInfo$ = this.workerService.getWorkerInfo(this.route.snapshot.params['address'], this.route.snapshot.params['workerName'], this.route.snapshot.params['workerId']).pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
this.chartData$ = combineLatest([ this.workerInfo$, this.networkInfo$]).pipe(
map(([workerInfo, networkInfo]) => {
this.networkInfo = networkInfo;
return {
labels: workerInfo.chartData.map((d: any) => d.label),
datasets: [
{
label: workerInfo.name,
data: workerInfo.chartData.map((d: any) => d.data),
fill: false,
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
borderColor: documentStyle.getPropertyValue('--primary-color'),
tension: .4,
pointRadius: 1,
borderWidth: 1
}
]
}
})
);
this.chartOptions = {
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day'
},
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder,
drawBorder: false
}
},
y: {
ticks: {
color: textColorSecondary,
callback: (value: number) => {
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
}
},
grid: {
color: surfaceBorder,
drawBorder: false
}
}
}
};
}
}
+4
View File
@@ -0,0 +1,4 @@
export interface MenuChangeEvent {
key: string;
routeEvent?: boolean;
}
+3
View File
@@ -0,0 +1,3 @@
<div>
</div>
+10
View File
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { LayoutService } from "./service/app.layout.service";
@Component({
selector: 'app-footer',
templateUrl: './app.footer.component.html'
})
export class AppFooterComponent {
constructor(public layoutService: LayoutService) { }
}
+14
View File
@@ -0,0 +1,14 @@
<div class="layout-wrapper" [ngClass]="containerClass">
<app-topbar></app-topbar>
<div class="layout-sidebar">
<app-sidebar></app-sidebar>
</div>
<div class="layout-main-container">
<div class="layout-main">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
</div>
<!-- <app-config></app-config> -->
<div class="layout-mask"></div>
</div>
+121
View File
@@ -0,0 +1,121 @@
import { Component, OnDestroy, Renderer2, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, Subscription } from 'rxjs';
import { LayoutService } from "./service/app.layout.service";
import { AppSidebarComponent } from "./app.sidebar.component";
import { AppTopBarComponent } from './app.topbar.component';
@Component({
selector: 'app-layout',
templateUrl: './app.layout.component.html'
})
export class AppLayoutComponent implements OnDestroy {
overlayMenuOpenSubscription: Subscription;
menuOutsideClickListener: any;
profileMenuOutsideClickListener: any;
@ViewChild(AppSidebarComponent) appSidebar!: AppSidebarComponent;
@ViewChild(AppTopBarComponent) appTopbar!: AppTopBarComponent;
constructor(public layoutService: LayoutService, public renderer: Renderer2, public router: Router) {
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
if (!this.menuOutsideClickListener) {
this.menuOutsideClickListener = this.renderer.listen('document', 'click', event => {
const isOutsideClicked = !(this.appSidebar.el.nativeElement.isSameNode(event.target) || this.appSidebar.el.nativeElement.contains(event.target)
|| this.appTopbar.menuButton.nativeElement.isSameNode(event.target) || this.appTopbar.menuButton.nativeElement.contains(event.target));
if (isOutsideClicked) {
this.hideMenu();
}
});
}
if (!this.profileMenuOutsideClickListener) {
this.profileMenuOutsideClickListener = this.renderer.listen('document', 'click', event => {
const isOutsideClicked = !(this.appTopbar.menu.nativeElement.isSameNode(event.target) || this.appTopbar.menu.nativeElement.contains(event.target)
|| this.appTopbar.topbarMenuButton.nativeElement.isSameNode(event.target) || this.appTopbar.topbarMenuButton.nativeElement.contains(event.target));
if (isOutsideClicked) {
this.hideProfileMenu();
}
});
}
if (this.layoutService.state.staticMenuMobileActive) {
this.blockBodyScroll();
}
});
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
this.hideMenu();
this.hideProfileMenu();
});
}
hideMenu() {
this.layoutService.state.overlayMenuActive = false;
this.layoutService.state.staticMenuMobileActive = false;
this.layoutService.state.menuHoverActive = false;
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
this.menuOutsideClickListener = null;
}
this.unblockBodyScroll();
}
hideProfileMenu() {
this.layoutService.state.profileSidebarVisible = false;
if (this.profileMenuOutsideClickListener) {
this.profileMenuOutsideClickListener();
this.profileMenuOutsideClickListener = null;
}
}
blockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.add('blocked-scroll');
}
else {
document.body.className += ' blocked-scroll';
}
}
unblockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.remove('blocked-scroll');
}
else {
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' +
'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
get containerClass() {
return {
'layout-theme-light': this.layoutService.config.colorScheme === 'light',
'layout-theme-dark': this.layoutService.config.colorScheme === 'dark',
'layout-overlay': this.layoutService.config.menuMode === 'overlay',
'layout-static': this.layoutService.config.menuMode === 'static',
'layout-static-inactive': this.layoutService.state.staticMenuDesktopInactive && this.layoutService.config.menuMode === 'static',
'layout-overlay-active': this.layoutService.state.overlayMenuActive,
'layout-mobile-active': this.layoutService.state.staticMenuMobileActive,
'p-input-filled': this.layoutService.config.inputStyle === 'filled',
'p-ripple-disabled': !this.layoutService.config.ripple
}
}
ngOnDestroy() {
if (this.overlayMenuOpenSubscription) {
this.overlayMenuOpenSubscription.unsubscribe();
}
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
}
}
}
+47
View File
@@ -0,0 +1,47 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { BadgeModule } from 'primeng/badge';
import { InputSwitchModule } from 'primeng/inputswitch';
import { InputTextModule } from 'primeng/inputtext';
import { RadioButtonModule } from 'primeng/radiobutton';
import { RippleModule } from 'primeng/ripple';
import { SidebarModule } from 'primeng/sidebar';
import { AppFooterComponent } from './app.footer.component';
import { AppLayoutComponent } from './app.layout.component';
import { AppMenuComponent } from './app.menu.component';
import { AppMenuitemComponent } from './app.menuitem.component';
import { AppSidebarComponent } from './app.sidebar.component';
import { AppTopBarComponent } from './app.topbar.component';
//import { AppConfigModule } from './config/config.module';
@NgModule({
declarations: [
AppMenuitemComponent,
AppTopBarComponent,
AppFooterComponent,
AppMenuComponent,
AppSidebarComponent,
AppLayoutComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
BrowserAnimationsModule,
InputTextModule,
SidebarModule,
BadgeModule,
RadioButtonModule,
InputSwitchModule,
RippleModule,
RouterModule,
// AppConfigModule
],
exports: [AppLayoutComponent]
})
export class AppLayoutModule { }
+7
View File
@@ -0,0 +1,7 @@
<ul class="layout-menu">
<ng-container *ngFor="let item of model$ | async; let i = index;">
<li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
<li *ngIf="item.separator" class="menu-separator"></li>
</ng-container>
</ul>
+72
View File
@@ -0,0 +1,72 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { filter, map, mergeMap, Observable, startWith } from 'rxjs';
import { LayoutService } from './service/app.layout.service';
@Component({
selector: 'app-menu',
templateUrl: './app.menu.component.html'
})
export class AppMenuComponent implements OnInit {
private params$!: Observable<any>;
public model$!: Observable<any[]>;
constructor(public layoutService: LayoutService, private router: Router, private activatedRoute: ActivatedRoute) { }
ngOnInit() {
const rootRoute = this.activatedRoute.root;
const initialParams = this.extractRouteParams(rootRoute.snapshot);
this.params$ = this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
}),
filter((route) => route.outlet === 'primary'), // Adjust the outlet name if necessary
mergeMap((route) => route.params),
startWith(initialParams)
);
this.model$ = this.params$.pipe(map(params => {
return [
{
label: 'Dashboard',
items: [
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: [params.address] },
{ label: 'Settings', icon: 'pi pi-fw pi-cog', routerLink: [params.address, 'settings'] }
]
}
];
}))
}
private extractRouteParams(routeSnapshot: ActivatedRouteSnapshot): any {
let route = routeSnapshot;
let params = {};
while (route.firstChild) {
route = route.firstChild;
}
if (route && route.params) {
params = route.params;
}
return params;
}
}
+23
View File
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { MenuChangeEvent } from './api/menuchangeevent';
@Injectable({
providedIn: 'root'
})
export class MenuService {
private menuSource = new Subject<MenuChangeEvent>();
private resetSource = new Subject();
menuSource$ = this.menuSource.asObservable();
resetSource$ = this.resetSource.asObservable();
onMenuStateChange(event: MenuChangeEvent) {
this.menuSource.next(event);
}
reset() {
this.resetSource.next(true);
}
}
+148
View File
@@ -0,0 +1,148 @@
import { ChangeDetectorRef, Component, Host, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { MenuService } from './app.menu.service';
import { LayoutService } from './service/app.layout.service';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '[app-menuitem]',
template: `
<ng-container>
<div *ngIf="root && item.visible !== false" class="layout-menuitem-root-text">{{item.label}}</div>
<a *ngIf="(!item.routerLink || item.items) && item.visible !== false" [attr.href]="item.url" (click)="itemClick($event)"
[ngClass]="item.class" [attr.target]="item.target" tabindex="0" pRipple>
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
<span class="layout-menuitem-text">{{item.label}}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<a *ngIf="(item.routerLink && !item.items) && item.visible !== false" (click)="itemClick($event)" [ngClass]="item.class"
[routerLink]="item.routerLink" routerLinkActive="active-route" [routerLinkActiveOptions]="item.routerLinkActiveOptions||{ paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' }"
[fragment]="item.fragment" [queryParamsHandling]="item.queryParamsHandling" [preserveFragment]="item.preserveFragment"
[skipLocationChange]="item.skipLocationChange" [replaceUrl]="item.replaceUrl" [state]="item.state" [queryParams]="item.queryParams"
[attr.target]="item.target" tabindex="0" pRipple>
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
<span class="layout-menuitem-text">{{item.label}}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<ul *ngIf="item.items && item.visible !== false" [@children]="submenuAnimation">
<ng-template ngFor let-child let-i="index" [ngForOf]="item.items">
<li app-menuitem [item]="child" [index]="i" [parentKey]="key" [class]="child.badgeClass"></li>
</ng-template>
</ul>
</ng-container>
`,
animations: [
trigger('children', [
state('collapsed', style({
height: '0'
})),
state('expanded', style({
height: '*'
})),
transition('collapsed <=> expanded', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)'))
])
]
})
export class AppMenuitemComponent implements OnInit, OnDestroy {
@Input() item: any;
@Input() index!: number;
@Input() @HostBinding('class.layout-root-menuitem') root!: boolean;
@Input() parentKey!: string;
active = false;
menuSourceSubscription: Subscription;
menuResetSubscription: Subscription;
key: string = "";
constructor(public layoutService: LayoutService, private cd: ChangeDetectorRef, public router: Router, private menuService: MenuService) {
this.menuSourceSubscription = this.menuService.menuSource$.subscribe(value => {
Promise.resolve(null).then(() => {
if (value.routeEvent) {
this.active = (value.key === this.key || value.key.startsWith(this.key + '-')) ? true : false;
}
else {
if (value.key !== this.key && !value.key.startsWith(this.key + '-')) {
this.active = false;
}
}
});
});
this.menuResetSubscription = this.menuService.resetSource$.subscribe(() => {
this.active = false;
});
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(params => {
if (this.item.routerLink) {
this.updateActiveStateFromRoute();
}
});
}
ngOnInit() {
this.key = this.parentKey ? this.parentKey + '-' + this.index : String(this.index);
if (this.item.routerLink) {
this.updateActiveStateFromRoute();
}
}
updateActiveStateFromRoute() {
let activeRoute = this.router.isActive(this.item.routerLink[0], { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' });
if (activeRoute) {
this.menuService.onMenuStateChange({ key: this.key, routeEvent: true });
}
}
itemClick(event: Event) {
// avoid processing disabled items
if (this.item.disabled) {
event.preventDefault();
return;
}
// execute command
if (this.item.command) {
this.item.command({ originalEvent: event, item: this.item });
}
// toggle active state
if (this.item.items) {
this.active = !this.active;
}
this.menuService.onMenuStateChange({ key: this.key });
}
get submenuAnimation() {
return this.root ? 'expanded' : (this.active ? 'expanded' : 'collapsed');
}
@HostBinding('class.active-menuitem')
get activeClass() {
return this.active && !this.root;
}
ngOnDestroy() {
if (this.menuSourceSubscription) {
this.menuSourceSubscription.unsubscribe();
}
if (this.menuResetSubscription) {
this.menuResetSubscription.unsubscribe();
}
}
}
@@ -0,0 +1 @@
<app-menu></app-menu>
+11
View File
@@ -0,0 +1,11 @@
import { Component, ElementRef } from '@angular/core';
import { LayoutService } from "./service/app.layout.service";
@Component({
selector: 'app-sidebar',
templateUrl: './app.sidebar.component.html'
})
export class AppSidebarComponent {
constructor(public layoutService: LayoutService, public el: ElementRef) { }
}
+31
View File
@@ -0,0 +1,31 @@
<div class="layout-topbar">
<a class="layout-topbar-logo" routerLink="">
<img src="assets/layout/images/logo.svg" alt="logo">
<span>Public Pool</span>
</a>
<button #menubutton class="p-link layout-menu-button layout-topbar-button" (click)="layoutService.onMenuToggle()">
<i class="pi pi-bars"></i>
</button>
<button #topbarmenubutton class="p-link layout-topbar-menu-button layout-topbar-button"
(click)="layoutService.showProfileSidebar()">
<i class="pi pi-ellipsis-v"></i>
</button>
<!-- <div #topbarmenu class="layout-topbar-menu"
[ngClass]="{'layout-topbar-menu-mobile-active': layoutService.state.profileSidebarVisible}">
<button class="p-link layout-topbar-button">
<i class="pi pi-calendar"></i>
<span>Calendar</span>
</button>
<button class="p-link layout-topbar-button">
<i class="pi pi-user"></i>
<span>Profile</span>
</button>
<button class="p-link layout-topbar-button" [routerLink]="'/documentation'">
<i class="pi pi-cog"></i>
<span>Settings</span>
</button>
</div> -->
</div>
+20
View File
@@ -0,0 +1,20 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { LayoutService } from "./service/app.layout.service";
@Component({
selector: 'app-topbar',
templateUrl: './app.topbar.component.html'
})
export class AppTopBarComponent {
items!: MenuItem[];
@ViewChild('menubutton') menuButton!: ElementRef;
@ViewChild('topbarmenubutton') topbarMenuButton!: ElementRef;
@ViewChild('topbarmenu') menu!: ElementRef;
constructor(public layoutService: LayoutService) { }
}
@@ -0,0 +1,100 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
export interface AppConfig {
inputStyle: string;
colorScheme: string;
theme: string;
ripple: boolean;
menuMode: string;
scale: number;
}
interface LayoutState {
staticMenuDesktopInactive: boolean;
overlayMenuActive: boolean;
profileSidebarVisible: boolean;
configSidebarVisible: boolean;
staticMenuMobileActive: boolean;
menuHoverActive: boolean;
}
@Injectable({
providedIn: 'root',
})
export class LayoutService {
config: AppConfig = {
ripple: false,
inputStyle: 'outlined',
menuMode: 'static',
colorScheme: 'light',
theme: 'lara-light-indigo',
scale: 14,
};
state: LayoutState = {
staticMenuDesktopInactive: false,
overlayMenuActive: false,
profileSidebarVisible: false,
configSidebarVisible: false,
staticMenuMobileActive: false,
menuHoverActive: false
};
private configUpdate = new Subject<AppConfig>();
private overlayOpen = new Subject<any>();
configUpdate$ = this.configUpdate.asObservable();
overlayOpen$ = this.overlayOpen.asObservable();
onMenuToggle() {
if (this.isOverlay()) {
this.state.overlayMenuActive = !this.state.overlayMenuActive;
if (this.state.overlayMenuActive) {
this.overlayOpen.next(null);
}
}
if (this.isDesktop()) {
this.state.staticMenuDesktopInactive = !this.state.staticMenuDesktopInactive;
}
else {
this.state.staticMenuMobileActive = !this.state.staticMenuMobileActive;
if (this.state.staticMenuMobileActive) {
this.overlayOpen.next(null);
}
}
}
showProfileSidebar() {
this.state.profileSidebarVisible = !this.state.profileSidebarVisible;
if (this.state.profileSidebarVisible) {
this.overlayOpen.next(null);
}
}
showConfigSidebar() {
this.state.configSidebarVisible = true;
}
isOverlay() {
return this.config.menuMode === 'overlay';
}
isDesktop() {
return window.innerWidth > 991;
}
isMobile() {
return !this.isDesktop();
}
onConfigUpdate() {
this.configUpdate.next(this.config);
}
}
@@ -0,0 +1,8 @@
import { AverageTimeToBlockPipe } from './average-time-to-block.pipe';
describe('AverageTimeToBlockPipe', () => {
it('create an instance', () => {
const pipe = new AverageTimeToBlockPipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,34 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DateAgoPipe } from './date-ago.pipe';
@Pipe({
name: 'averageTimeToBlock',
standalone: true
})
export class AverageTimeToBlockPipe implements PipeTransform {
private static _this = new AverageTimeToBlockPipe();
private static _dateAgo = new DateAgoPipe();
public static transform(value: number, difficulty: number): string {
return this._this.transform(value, difficulty);
}
public transform(value: number, difficulty: number): string {
const blockTimeInSeconds = this.calculateBlockTime(value, difficulty);
if(blockTimeInSeconds > 8000000000000){
return '~∞';
}
const date = new Date(new Date().getTime() + (blockTimeInSeconds * 1000));
return AverageTimeToBlockPipe._dateAgo.transform(date);
}
private calculateBlockTime(hashRate: number, difficulty: number): number {
const hashesPerDifficulty = Math.pow(2, 32);
// Calculate the average block time in seconds
const averageBlockTime = (difficulty * hashesPerDifficulty) / hashRate;
return averageBlockTime;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { DateAgoPipe } from './date-ago.pipe';
describe('DateAgoPipe', () => {
it('create an instance', () => {
const pipe = new DateAgoPipe();
expect(pipe).toBeTruthy();
});
});
+40
View File
@@ -0,0 +1,40 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'dateAgo',
pure: true
})
export class DateAgoPipe implements PipeTransform {
transform(value: any, args?: any): any {
if (value) {
const seconds = Math.abs(Math.floor((+new Date() - +new Date(value)) / 1000));
if (seconds < 29) // less than 30 seconds ago will show as 'Just now'
return 'Just now';
const intervals: { [key: string]: number } = {
'year': 31536000,
'month': 2592000,
'week': 604800,
'day': 86400,
'hour': 3600,
'minute': 60,
'second': 1
};
let counter;
for (const i in intervals) {
const number = (seconds / intervals[i]).toFixed(1);
counter = Math.floor(seconds / intervals[i]);
if (counter > 0)
return number + ' ' + i + 's'; // plural (2 days ago)
// if (counter === 1) {
// return number + ' ' + i + ''; // singular (1 day ago)
// } else {
// return number + ' ' + i + 's'; // plural (2 days ago)
// }
}
}
return value;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { HashSuffixPipe } from './hash-suffix.pipe';
describe('HashSuffixPipe', () => {
it('create an instance', () => {
const pipe = new HashSuffixPipe();
expect(pipe).toBeTruthy();
});
});
+33
View File
@@ -0,0 +1,33 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'hashSuffix'
})
export class HashSuffixPipe implements PipeTransform {
private static _this = new HashSuffixPipe();
public static transform(value: number): string {
return this._this.transform(value);
}
public transform(value: number): string {
if (value == null || value < 0) {
return '0';
}
const suffixes = [' H/s', ' KH/s', ' MH/s', ' GH/s', ' TH/s', ' PH/s', ' EH/s', ' ZH/s', ' YH/s', ' RH/s', ' QH/s'];
let power = Math.floor(Math.log10(value) / 3);
if (power < 0) {
power = 0;
}
const scaledValue = value / Math.pow(1000, power);
const suffix = suffixes[power];
return scaledValue.toFixed(1) + suffix;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { NumberSuffixPipe } from './number-suffix.pipe';
describe('NumberSuffixPipe', () => {
it('create an instance', () => {
const pipe = new NumberSuffixPipe();
expect(pipe).toBeTruthy();
});
});
+27
View File
@@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'numberSuffix',
pure: true
})
export class NumberSuffixPipe implements PipeTransform {
transform(value: number): string {
const suffixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
if (value == null || value < 0) {
return '0';
}
let power = Math.floor(Math.log10(value) / 3);
if (power < 0) {
power = 0;
}
const scaledValue = value / Math.pow(1000, power);
const suffix = suffixes[power];
return scaledValue.toFixed(2) + suffix;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class AppService {
constructor(
private httpClient: HttpClient
) { }
public getInfo() {
return this.httpClient.get(`${environment.API_URL}/api/info`) as Observable<any>;
}
public getNetworkInfo() {
return this.httpClient.get(`${environment.API_URL}/api/network`) as Observable<any>;
}
public getInfoChart() {
return this.httpClient.get(`${environment.API_URL}/api/info/chart`) as Observable<any>;
}
}
+16
View File
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ClientService } from './client.service';
describe('ClientService', () => {
let service: ClientService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ClientService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+22
View File
@@ -0,0 +1,22 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ClientService {
constructor(
private httpClient: HttpClient
) { }
public getClientInfo(address: string) {
return this.httpClient.get(`${environment.API_URL}/api/client/${address}`);
}
public getClientInfoChart(address: string) {
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/chart`) as Observable<any[]>;
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { LocalStorageService } from './local-storage.service';
describe('LocalStorageService', () => {
let service: LocalStorageService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LocalStorageService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+44
View File
@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, shareReplay } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LocalStorageService {
private PARTICLES = 'PARTICLES';
private _particles$: BehaviorSubject<boolean>;
public particles$: Observable<boolean>;
constructor() {
this._particles$ = new BehaviorSubject<boolean>(this.getParticles());
this.particles$ = this._particles$.asObservable().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
}
private get(key: string): string | null {
return localStorage.getItem(key);
}
private set(key: string, value: string) {
localStorage.setItem(key, value);
}
private remove(key: string): void {
localStorage.removeItem(key);
}
public getParticles(): boolean {
const result = this.get(this.PARTICLES);
return result == null || JSON.parse(result)?.particles === true;
}
public setParticles(particles: boolean) {
this.set(this.PARTICLES, JSON.stringify({ particles }));
this._particles$.next(particles);
}
}
+16
View File
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WorkerService } from './worker.service';
describe('WorkerService', () => {
let service: WorkerService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(WorkerService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+23
View File
@@ -0,0 +1,23 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class WorkerService {
constructor(
private httpClient: HttpClient
) { }
public getGroupWorkerInfo(address: string, workerName: string): Observable<any> {
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerName}`);
}
public getWorkerInfo(address: string, workerName: string, workerId: string): Observable<any> {
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerName}/${workerId}`);
}
}
@@ -0,0 +1,13 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { validate } from 'bitcoin-address-validation';
export function bitcoinAddressValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
if (validate(control.value)) {
return null;
}
return { ['bitcoin-address']: true };
};
}
View File

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