diff --git a/README.md b/README.md index 3c88968..2526751 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 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. @@ -25,3 +29,11 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u ## 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 +``` diff --git a/package-lock.json b/package-lock.json index 6bc192d..a4fa29e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.1.1", - "@angular/cli": "~16.1.1", + "@angular/cli": "^16.1.4", "@angular/compiler-cli": "^16.1.2", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.2.0", @@ -42,7 +42,8 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "typescript": "~5.1.3" + "typescript": "~5.1.3", + "webpack-bundle-analyzer": "^4.9.0" } }, "node_modules/@ampproject/remapping": { @@ -241,12 +242,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.1.tgz", - "integrity": "sha512-s8LFr0m4ILEpJuQj78fCWKocnRleA3MWJU1Q5LZloCcUB8fdDvaPNCt5s0VWC2Sp+4OCxJaSN3kjjcFbCYFvTA==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.4.tgz", + "integrity": "sha512-yjRgwHAfFaeuimgbQtjwSUyXzEHpMSdTRb2zg+TOp6skoGvHOG8xXFJ7DjBkSMeAQdFF0fkxhPS9YmlxqNc+7A==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.1", + "@angular-devkit/core": "16.1.4", "jsonc-parser": "3.2.0", "magic-string": "0.30.0", "ora": "5.4.1", @@ -258,6 +259,32 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.4.tgz", + "integrity": "sha512-WCAzNi9LxpFIi2WVPaJQd2kHPqCnCexWzUZN05ltJuBGCQL1O+LgRHGwnQ4WZoqmrF5tcWt2a3GFtJ3DgMc1hw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/animations": { "version": "16.1.2", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.1.2.tgz", @@ -273,15 +300,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.1.tgz", - "integrity": "sha512-QrTgMqMnamteZu2x3JhLMo6wBjI05zMr9RQfMHWq4UrUpTqBcHAMqJIKSSbvrtuRbolLrQyLorwxzlmEOfEmbQ==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.4.tgz", + "integrity": "sha512-coSOLVLpOCOD5q9K9EAFFMrTES+HtdJiLy/iI9kdKNCKWUJpm8/svZ3JZOej3vPxYEp0AokXNOwORQnX21/qZQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.1", - "@angular-devkit/core": "16.1.1", - "@angular-devkit/schematics": "16.1.1", - "@schematics/angular": "16.1.1", + "@angular-devkit/architect": "0.1601.4", + "@angular-devkit/core": "16.1.4", + "@angular-devkit/schematics": "16.1.4", + "@schematics/angular": "16.1.4", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -293,7 +320,7 @@ "ora": "5.4.1", "pacote": "15.2.0", "resolve": "1.22.2", - "semver": "7.5.1", + "semver": "7.5.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -306,6 +333,80 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1601.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.4.tgz", + "integrity": "sha512-OOSbNlDy+Q3jY0oFHaq8kkna9HYI1zaS8IHeCIDP6T/ZIAVad4+HqXAL4SKQrKJikkoBQv1Z/eaDBL5XPFK9Bw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.1.4", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.4.tgz", + "integrity": "sha512-WCAzNi9LxpFIi2WVPaJQd2kHPqCnCexWzUZN05ltJuBGCQL1O+LgRHGwnQ4WZoqmrF5tcWt2a3GFtJ3DgMc1hw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@angular/common": { "version": "16.1.2", "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.2.tgz", @@ -3027,14 +3128,20 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, "node_modules/@schematics/angular": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.1.tgz", - "integrity": "sha512-mJo7FxH3dekG7m4hHW5PyWbiCUaU+DSW93j+cikEksda+Qt6NaEX0hM0W3DjH7O+BnEg6dbAEd2GDSN/0XQghw==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.4.tgz", + "integrity": "sha512-XfoeL+aBVIR/DzgVKGVhHW/TGQnqWvngyJVuCwXEVWzNfjxHYFkchXa78OItpAvTEr6/Y0Me9FQVAGVA4mMUyg==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.1", - "@angular-devkit/schematics": "16.1.1", + "@angular-devkit/core": "16.1.4", + "@angular-devkit/schematics": "16.1.4", "jsonc-parser": "3.2.0" }, "engines": { @@ -3043,6 +3150,32 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.4.tgz", + "integrity": "sha512-WCAzNi9LxpFIi2WVPaJQd2kHPqCnCexWzUZN05ltJuBGCQL1O+LgRHGwnQ4WZoqmrF5tcWt2a3GFtJ3DgMc1hw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sigstore/protobuf-specs": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz", @@ -3533,6 +3666,15 @@ "acorn": "^8" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", @@ -5035,6 +5177,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6000,6 +6148,21 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8532,6 +8695,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -10094,6 +10266,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -10758,6 +10944,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11604,6 +11799,130 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz", + "integrity": "sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-dev-middleware": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", diff --git a/package.json b/package.json index 5320c56..fb59ce6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "start": "ng serve --proxy-config proxy.config.local.json ", "build": "ng build --configuration=production", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "bundle-report": "ng build --configuration=production --stats-json && webpack-bundle-analyzer dist/public-pool-ui/stats.json" }, "private": true, "dependencies": { @@ -34,8 +35,8 @@ "zone.js": "~0.13.1" }, "devDependencies": { + "@angular/cli": "^16.1.4", "@angular-devkit/build-angular": "^16.1.1", - "@angular/cli": "~16.1.1", "@angular/compiler-cli": "^16.1.2", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.2.0", @@ -44,6 +45,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "typescript": "~5.1.3" + "typescript": "~5.1.3", + "webpack-bundle-analyzer": "^4.9.0" } -} +} \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bf5f12c..3e4eabc 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -2,6 +2,7 @@ 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'; @@ -19,6 +20,10 @@ const routes: Routes = [ { path: ':address', children: [ + { + path: 'settings', + component: SettingsComponent + }, { path: '', component: DashboardComponent, @@ -26,6 +31,7 @@ const routes: Routes = [ { path: ':workerName', children: [ + { path: '', component: WorkerGroupComponent @@ -35,7 +41,8 @@ const routes: Routes = [ component: WorkerComponent } ] - } + }, + ] } diff --git a/src/app/app.component.html b/src/app/app.component.html index 6074af9..e7c1f29 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e79f631..104b3b3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,7 @@ import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { LocalStorageService } from './services/local-storage.service'; @Component({ selector: 'app-root', @@ -7,4 +10,9 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'public-pool-ui'; + + public particles$: Observable; + constructor(private localService: LocalStorageService) { + this.particles$ = this.localService.particles$; + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d9b4091..c7e548e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,7 +16,10 @@ import { WorkerGroupComponent } from './components/worker-group/worker-group.com 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 { SettingsComponent } from './components/settings/settings.component'; +import { UserAgentPipe } from './pipes/user-agent.pipe'; @@ -29,7 +32,10 @@ import { NumberSuffixPipe } from './pipes/number-suffix.pipe'; NumberSuffixPipe, DateAgoPipe, WorkerGroupComponent, - BackgroundParticlesComponent + BackgroundParticlesComponent, + HashSuffixPipe, + SettingsComponent, + UserAgentPipe ], imports: [ CommonModule, diff --git a/src/app/components/background-particles/background-particles.component.ts b/src/app/components/background-particles/background-particles.component.ts index 69c9645..5565a20 100644 --- a/src/app/components/background-particles/background-particles.component.ts +++ b/src/app/components/background-particles/background-particles.component.ts @@ -32,7 +32,7 @@ export class BackgroundParticlesComponent implements OnInit { const lineColor: string = '#afccfa'; this.particleOptions = { - fpsLimit: this.deviceService.isDesktop() ? 30 : 1, + fpsLimit: 30, detectRetina: true, background: { position: "50% 50%", @@ -75,11 +75,11 @@ export class BackgroundParticlesComponent implements OnInit { } particlesLoaded(container: Container): void { - console.log(container); + } async particlesInit(engine: Engine): Promise { - console.log(engine); + // 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 diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index 303889b..8b8a1fc 100644 --- a/src/app/components/dashboard/dashboard.component.html +++ b/src/app/components/dashboard/dashboard.component.html @@ -1,4 +1,22 @@ +
+
+
+
+
+ Best Difficulty +
{{clientInfo.bestDifficulty | numberSuffix}} +
+
+
+ +
+
+ {{clientInfo.bestDifficulty | number}} +
+
+
@@ -31,7 +49,7 @@ {{getSessionCount(worker.name, clientInfo.workers)}} Sessions - {{getTotalHashRate(worker.name, clientInfo.workers)}} GH/s + {{getTotalHashRate(worker.name, clientInfo.workers) | hashSuffix}} {{getBestDifficulty(worker.name, clientInfo.workers) | numberSuffix}} @@ -47,7 +65,7 @@ {{worker.sessionId}} - {{worker.hashRate}} GH/s + {{worker.hashRate | hashSuffix}} {{worker.bestDifficulty | numberSuffix}} {{worker.startTime | dateAgo}} diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts index 2261b33..a324592 100644 --- a/src/app/components/dashboard/dashboard.component.ts +++ b/src/app/components/dashboard/dashboard.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Observable, shareReplay } from 'rxjs'; +import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe'; import { ClientService } from '../../services/client.service'; @Component({ @@ -18,6 +19,8 @@ export class DashboardComponent { public chartOptions: any; + + constructor(private clientService: ClientService, private route: ActivatedRoute) { this.address = this.route.snapshot.params['address']; this.clientInfo$ = this.clientService.getClientInfo(this.address).pipe( @@ -61,18 +64,22 @@ export class DashboardComponent { label: '2 Hour', data: hourlyData, fill: false, - backgroundColor: documentStyle.getPropertyValue('--primary-color'), - borderColor: documentStyle.getPropertyValue('--primary-color'), - tension: .4 + 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('--bluegray-700'), - borderColor: documentStyle.getPropertyValue('--bluegray-700'), - tension: .4 + backgroundColor: documentStyle.getPropertyValue('--primary-color'), + borderColor: documentStyle.getPropertyValue('--primary-color'), + tension: .4, + pointRadius: 1, + borderWidth: 1 }, ] @@ -108,7 +115,7 @@ export class DashboardComponent { y: { ticks: { color: textColorSecondary, - callback: (value: number) => value + ' GH/s', + callback: (value: number) => HashSuffixPipe.transform(value) }, grid: { color: surfaceBorder, diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html new file mode 100644 index 0000000..676640d --- /dev/null +++ b/src/app/components/settings/settings.component.html @@ -0,0 +1,5 @@ +
+
Background Particles
+ +
\ No newline at end of file diff --git a/src/app/components/settings/settings.component.scss b/src/app/components/settings/settings.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/settings/settings.component.spec.ts b/src/app/components/settings/settings.component.spec.ts new file mode 100644 index 0000000..c2333ad --- /dev/null +++ b/src/app/components/settings/settings.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SettingsComponent] + }); + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts new file mode 100644 index 0000000..8519d8e --- /dev/null +++ b/src/app/components/settings/settings.component.ts @@ -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; + } +} diff --git a/src/app/components/splash/splash.component.html b/src/app/components/splash/splash.component.html index 1197e5c..d91830d 100644 --- a/src/app/components/splash/splash.component.html +++ b/src/app/components/splash/splash.component.html @@ -1,26 +1,42 @@ -
+
-
-

Public Pool

-
logo
+ +
+

Public Pool

+
+ +
+ Fully Open Source Solo Bitcoin Mining Pool +
+
- stratum+tcp://public-pool-web.airdns.org:21496 + stratum+tcp://public-pool.airdns.org:21496
username: <your BTC address>.<worker name>, password: x

- 1.8% fee includes donations to open source Bitcoin mining + Pleb mining (under 50TH/s miners) mine with NO FEES* +
+ Miners with > 50TH/s will incur a 1.5% fee, a portion of which will go back into open source Bitcoin mining* +

No second best.
@@ -32,6 +48,65 @@
+ + * Based on a machine-by-machine basis +
+
+ +
+ +
+
+ +
+
+ +
+ + +
+
+

Devices

+ + + + Device + Currently Working + + + + + {{ userAgent.userAgent | userAgent }} + {{ userAgent.count }} + + + + +
+
+ +
+
+

Found Blocks

+ + + + Height + Address + Worker + Session + + + + + {{ block.height }} + {{ block.minerAddress }} + {{ block.worker }} + {{ block.sessionId }} + + + +
diff --git a/src/app/components/splash/splash.component.scss b/src/app/components/splash/splash.component.scss index 89cc760..0de12a6 100644 --- a/src/app/components/splash/splash.component.scss +++ b/src/app/components/splash/splash.component.scss @@ -2,6 +2,15 @@ min-width: 50%; } +.main { + margin-top: 60px; +} + .info { font-weight: bold; +} + +.card { + max-width: 1024px; + margin: 0 auto; } \ No newline at end of file diff --git a/src/app/components/splash/splash.component.ts b/src/app/components/splash/splash.component.ts index 040ff0d..46433a8 100644 --- a/src/app/components/splash/splash.component.ts +++ b/src/app/components/splash/splash.component.ts @@ -1,6 +1,9 @@ import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; +import { map, Observable, shareReplay } from 'rxjs'; +import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe'; +import { AppService } from '../../services/app.service'; import { bitcoinAddressValidator } from '../../validators/bitcoin-address.validator'; @Component({ @@ -11,7 +14,83 @@ import { bitcoinAddressValidator } from '../../validators/bitcoin-address.valida export class SplashComponent { public address: FormControl; - constructor() { + + public chartData$: Observable; + public blockData$: Observable; + public userAgents$: Observable; + + public chartOptions: any; + + constructor(private appService: AppService) { + + const info$ = this.appService.getInfo().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.blockData$ = info$.pipe(map(info => info.blockData)); + this.userAgents$ = info$.pipe(map(info => info.userAgents)); + this.chartData$ = info$.pipe( + map((info: any) => { + return { + labels: info.chartData.map((d: any) => d.label), + datasets: [ + { + label: 'Public-Pool Hashrate', + data: info.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 = { + 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) => HashSuffixPipe.transform(value) + }, + grid: { + color: surfaceBorder, + drawBorder: false + } + } + } + }; + } } diff --git a/src/app/components/worker-group/worker-group.component.ts b/src/app/components/worker-group/worker-group.component.ts index b42587f..5abb11c 100644 --- a/src/app/components/worker-group/worker-group.component.ts +++ b/src/app/components/worker-group/worker-group.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Observable, shareReplay } from 'rxjs'; +import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe'; import { WorkerService } from '../../services/worker.service'; @Component({ @@ -37,9 +38,11 @@ export class WorkerGroupComponent { label: workerInfo.name, data: workerInfo.chartData.map((d: any) => d.data), fill: false, - backgroundColor: documentStyle.getPropertyValue('--bluegray-700'), - borderColor: documentStyle.getPropertyValue('--bluegray-700'), - tension: .4 + backgroundColor: documentStyle.getPropertyValue('--primary-color'), + borderColor: documentStyle.getPropertyValue('--primary-color'), + tension: .4, + pointRadius: 1, + borderWidth: 1 } ] } @@ -60,9 +63,7 @@ export class WorkerGroupComponent { x: { type: 'time', time: { - unit: 'minute', // Set the unit to 'minute' - stepSize: 10, // Set the desired interval between labels in minutes - + unit: 'hour' }, ticks: { color: textColorSecondary @@ -75,7 +76,7 @@ export class WorkerGroupComponent { y: { ticks: { color: textColorSecondary, - callback: (value: number) => value + ' GH/s', + callback: (value: number) => HashSuffixPipe.transform(value) }, grid: { color: surfaceBorder, diff --git a/src/app/components/worker/worker.component.ts b/src/app/components/worker/worker.component.ts index 019a647..f438796 100644 --- a/src/app/components/worker/worker.component.ts +++ b/src/app/components/worker/worker.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Observable, shareReplay } from 'rxjs'; +import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe'; import { WorkerService } from '../../services/worker.service'; @Component({ @@ -37,9 +38,11 @@ export class WorkerComponent { label: workerInfo.name, data: workerInfo.chartData.map((d: any) => d.data), fill: false, - backgroundColor: documentStyle.getPropertyValue('--bluegray-700'), - borderColor: documentStyle.getPropertyValue('--bluegray-700'), - tension: .4 + backgroundColor: documentStyle.getPropertyValue('--primary-color'), + borderColor: documentStyle.getPropertyValue('--primary-color'), + tension: .4, + pointRadius: 1, + borderWidth: 1 } ] } @@ -60,9 +63,7 @@ export class WorkerComponent { x: { type: 'time', time: { - unit: 'minute', // Set the unit to 'minute' - stepSize: 10, // Set the desired interval between labels in minutes - + unit: 'hour' }, ticks: { color: textColorSecondary @@ -75,7 +76,7 @@ export class WorkerComponent { y: { ticks: { color: textColorSecondary, - callback: (value: number) => value + ' GH/s', + callback: (value: number) => HashSuffixPipe.transform(value) }, grid: { color: surfaceBorder, diff --git a/src/app/layout/app.footer.component.html b/src/app/layout/app.footer.component.html index 3311946..3a6cce8 100644 --- a/src/app/layout/app.footer.component.html +++ b/src/app/layout/app.footer.component.html @@ -1,3 +1,3 @@ -
- +
-
+ \ No newline at end of file diff --git a/src/app/layout/app.menu.component.ts b/src/app/layout/app.menu.component.ts index 95a5cfe..63dec8f 100644 --- a/src/app/layout/app.menu.component.ts +++ b/src/app/layout/app.menu.component.ts @@ -39,11 +39,12 @@ export class AppMenuComponent implements OnInit { return [ { - label: 'Home', + label: 'Dashboard', items: [ - { label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: [params.address] } + { label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: [params.address] }, + { label: 'Settings', icon: 'pi pi-fw pi-cog', routerLink: [params.address, 'settings'] } ] - }, + } ]; diff --git a/src/app/pipes/hash-suffix.pipe.spec.ts b/src/app/pipes/hash-suffix.pipe.spec.ts new file mode 100644 index 0000000..52cf57f --- /dev/null +++ b/src/app/pipes/hash-suffix.pipe.spec.ts @@ -0,0 +1,8 @@ +import { HashSuffixPipe } from './hash-suffix.pipe'; + +describe('HashSuffixPipe', () => { + it('create an instance', () => { + const pipe = new HashSuffixPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/hash-suffix.pipe.ts b/src/app/pipes/hash-suffix.pipe.ts new file mode 100644 index 0000000..f6a57cf --- /dev/null +++ b/src/app/pipes/hash-suffix.pipe.ts @@ -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']; + + 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; + } + + +} diff --git a/src/app/pipes/number-suffix.pipe.ts b/src/app/pipes/number-suffix.pipe.ts index 1265ec6..2544e4b 100644 --- a/src/app/pipes/number-suffix.pipe.ts +++ b/src/app/pipes/number-suffix.pipe.ts @@ -10,15 +10,18 @@ export class NumberSuffixPipe implements PipeTransform { const suffixes = ['', 'k', 'M', 'B', 'T', 'P', 'E']; - if (value === 0) { + if (value == null || value < 0) { return '0'; } - const power = Math.floor(Math.log10(value) / 3); + 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; + return scaledValue.toFixed(2) + suffix; } } diff --git a/src/app/pipes/user-agent.pipe.spec.ts b/src/app/pipes/user-agent.pipe.spec.ts new file mode 100644 index 0000000..fd60137 --- /dev/null +++ b/src/app/pipes/user-agent.pipe.spec.ts @@ -0,0 +1,8 @@ +import { UserAgentPipe } from './user-agent.pipe'; + +describe('UserAgentPipe', () => { + it('create an instance', () => { + const pipe = new UserAgentPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/user-agent.pipe.ts b/src/app/pipes/user-agent.pipe.ts new file mode 100644 index 0000000..32e138a --- /dev/null +++ b/src/app/pipes/user-agent.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'userAgent' +}) +export class UserAgentPipe implements PipeTransform { + + transform(value: string): string { + const valueLowerCase = value.toLowerCase(); + if (valueLowerCase.includes('bosminer')) { + return 'Braiins OS'; + } else { + return value; + } + } + +} diff --git a/src/app/services/app.service.ts b/src/app/services/app.service.ts new file mode 100644 index 0000000..8529316 --- /dev/null +++ b/src/app/services/app.service.ts @@ -0,0 +1,19 @@ +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; + } +} diff --git a/src/app/services/local-storage.service.spec.ts b/src/app/services/local-storage.service.spec.ts new file mode 100644 index 0000000..ba1dbd4 --- /dev/null +++ b/src/app/services/local-storage.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/services/local-storage.service.ts b/src/app/services/local-storage.service.ts new file mode 100644 index 0000000..deff011 --- /dev/null +++ b/src/app/services/local-storage.service.ts @@ -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; + public particles$: Observable; + + constructor() { + this._particles$ = new BehaviorSubject(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); + + } + + +} diff --git a/src/index.html b/src/index.html index 28b6634..7aea6a4 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,7 @@ - + diff --git a/src/prime-ng.module.ts b/src/prime-ng.module.ts index de16ee6..cf12562 100644 --- a/src/prime-ng.module.ts +++ b/src/prime-ng.module.ts @@ -13,12 +13,14 @@ import { DropdownModule } from 'primeng/dropdown'; import { DynamicDialogModule } from 'primeng/dynamicdialog'; import { FieldsetModule } from 'primeng/fieldset'; import { InputNumberModule } from 'primeng/inputnumber'; +import { InputSwitchModule } from 'primeng/inputswitch'; import { InputTextModule } from 'primeng/inputtext'; import { InputTextareaModule } from 'primeng/inputtextarea'; import { MenuModule } from 'primeng/menu'; import { OverlayPanelModule } from 'primeng/overlaypanel'; import { PanelModule } from 'primeng/panel'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { SelectButtonModule } from 'primeng/selectbutton'; import { StepsModule } from 'primeng/steps'; import { StyleClassModule } from 'primeng/styleclass'; import { TableModule } from 'primeng/table'; @@ -58,7 +60,9 @@ const primeNgModules = [ ChartModule, TagModule, StyleClassModule, - PanelModule + PanelModule, + SelectButtonModule, + InputSwitchModule ];