diff --git a/ui/angular.json b/ui/angular.json
index 2378f74..4bbe612 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -11,7 +11,7 @@
"prefix": "app",
"architect": {
"build": {
- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "ngx-build-plus:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
@@ -32,7 +32,8 @@
"scripts": [],
"outputHashing": "none",
"namedChunks": false,
- "optimization": true
+ "optimization": true,
+ "singleBundle": true
},
"configurations": {
"production": {
@@ -58,7 +59,7 @@
"defaultConfiguration": "production"
},
"serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "ngx-build-plus:dev-server",
"configurations": {
"production": {
"buildTarget": "ui:build:production"
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 0ef2ae2..4670658 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -36,6 +36,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "ngx-build-plus": "^20.0.0",
"typescript": "~5.9.2"
}
},
@@ -644,6 +645,7 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz",
"integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -4966,6 +4968,7 @@
"integrity": "sha512-XkgTwGhhrx+MVi2+TFO32d6Es5Uezzx7Y7B/e2ulDlj08bizxQj+9wkeLt5+bR8JWODHpEntZn/Xd5WvXnODGA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@angular-devkit/core": "20.3.9",
"@angular-devkit/schematics": "20.3.9",
@@ -5267,6 +5270,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
"integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -6309,6 +6313,7 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@@ -8879,6 +8884,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -10398,6 +10404,21 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
+ "node_modules/ngx-build-plus": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-20.0.0.tgz",
+ "integrity": "sha512-cm1ZMTACAN3DEqBt/alS84zwVGgL5HAl5Dk/wh7CPyGUBQnLaxiAhjFZ6iykxgSO3e9ebIZmDBvTC480piC1eA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webpack-merge": "^6.0.0"
+ },
+ "peerDependencies": {
+ "@angular-devkit/build-angular": ">=20.0.0",
+ "@schematics/angular": ">=20.0.0",
+ "rxjs": ">= 6.0.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -15464,6 +15485,7 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
diff --git a/ui/package.json b/ui/package.json
index 2bdcd47..450610f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -50,6 +50,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "ngx-build-plus": "^20.0.0",
"typescript": "~5.9.2"
}
}
diff --git a/ui/src/app/admin.component.css b/ui/src/app/admin.component.css
new file mode 100644
index 0000000..525e947
--- /dev/null
+++ b/ui/src/app/admin.component.css
@@ -0,0 +1,59 @@
+:host {
+ display: block;
+ font-family: sans-serif;
+ width: 100%;
+}
+
+.admin-card {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.header-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.card-error {
+ margin-bottom: 1rem;
+}
+
+.miner-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.miner-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ border: 1px solid #e0e0e0;
+}
+
+.section-title {
+ margin-top: 1.5rem;
+ border-top: 1px solid #e0e0e0;
+ padding-top: 1.5rem;
+}
+
+.path-list ul {
+ list-style: none;
+ padding: 0.5rem;
+ margin: 0;
+ font-family: monospace;
+ border-radius: 0.25rem;
+ border: 1px solid #e0e0e0;
+}
+
+.path-list li {
+ padding: 0.25rem 0;
+}
+
+.button-spinner {
+ font-size: 1em;
+ margin: 0;
+}
diff --git a/ui/src/app/admin.component.html b/ui/src/app/admin.component.html
new file mode 100644
index 0000000..f6e2ed7
--- /dev/null
+++ b/ui/src/app/admin.component.html
@@ -0,0 +1,97 @@
+
+@if (needsSetup) {
+
+
To begin, please install a miner from the list below.
+
Available Miners
+
+ @for (miner of manageableMiners; track miner.name) {
+
+ {{ miner.name }}
+
+ @if (actionInProgress === 'install-' + miner.name) {
+
+ } @else {
+
+ Install
+ }
+
+
+ } @empty {
+
+ Could not load available miners.
+
+ }
+
+
+}
+
+
+@if (!needsSetup) {
+
+ @if (error) {
+
+
+
+ An Error Occurred
+
+ {{ error }}
+
+ }
+
+
Manage Miners
+
+ @for (miner of manageableMiners; track miner.name) {
+
+ {{ miner.name }}
+ @if (miner.is_installed) {
+
+ @if (actionInProgress === 'uninstall-' + miner.name) {
+
+ } @else {
+
+ Uninstall
+ }
+
+ } @else {
+
+ @if (actionInProgress === 'install-' + miner.name) {
+
+ } @else {
+
+ Install
+ }
+
+ }
+
+ } @empty {
+
+ Could not load available miners.
+
+ }
+
+
+
Antivirus Whitelist Paths
+
+
To prevent antivirus software from interfering, please add the following paths to your exclusion list:
+
+ @for (path of whitelistPaths; track path) {
+ {{ path }}
+ } @empty {
+ - No paths to display. Install a miner to see required paths.
+ }
+
+
+
+}
diff --git a/ui/src/app/admin.component.ts b/ui/src/app/admin.component.ts
new file mode 100644
index 0000000..6f064ff
--- /dev/null
+++ b/ui/src/app/admin.component.ts
@@ -0,0 +1,130 @@
+import { Component, OnInit, Input, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { HttpClient, HttpClientModule, HttpErrorResponse } from '@angular/common/http';
+import { CommonModule } from '@angular/common';
+import { of, forkJoin } from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+
+// Define interfaces for our data structures
+interface InstallationDetails {
+ is_installed: boolean;
+ version: string;
+ path: string;
+ miner_binary: string;
+ config_path?: string;
+ type?: string;
+}
+
+interface AvailableMiner {
+ name: string;
+ description: string;
+}
+
+@Component({
+ selector: 'snider-mining-admin',
+ standalone: true,
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ imports: [CommonModule, HttpClientModule],
+ templateUrl: './admin.component.html',
+ styleUrls: ['./admin.component.css'],
+})
+export class MiningAdminComponent implements OnInit {
+ @Input() needsSetup: boolean = false; // Input to trigger setup mode
+
+ apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
+
+ error: string | null = null;
+ actionInProgress: string | null = null;
+ manageableMiners: any[] = [];
+ whitelistPaths: string[] = [];
+
+ constructor(private http: HttpClient) {}
+
+ ngOnInit(): void {
+ this.getAdminData();
+ }
+
+ private handleError(err: HttpErrorResponse, defaultMessage: string) {
+ console.error(err);
+ this.actionInProgress = null;
+ if (err.error && err.error.error) {
+ this.error = `${defaultMessage}: ${err.error.error}`;
+ } else {
+ this.error = `${defaultMessage}. Please check the console for details.`;
+ }
+ }
+
+ getAdminData(): void {
+ this.error = null;
+ forkJoin({
+ available: this.http.get(`${this.apiBaseUrl}/miners/available`),
+ info: this.http.get(`${this.apiBaseUrl}/info`).pipe(catchError(() => of({}))) // Gracefully handle info error
+ }).pipe(
+ map(({ available, info }) => {
+ const installedMap = new Map(
+ (info.installed_miners_info || []).map((m: InstallationDetails) => [this.getMinerType(m), m])
+ );
+
+ this.manageableMiners = available.map(availMiner => ({
+ ...availMiner,
+ is_installed: installedMap.get(availMiner.name)?.is_installed ?? false,
+ }));
+
+ const installedMiners = (info.installed_miners_info || []).filter((m: InstallationDetails) => m.is_installed);
+ this.updateWhitelistPaths(installedMiners, []);
+ }),
+ catchError(err => {
+ this.handleError(err, 'Could not load miner information');
+ return of(null);
+ })
+ ).subscribe();
+ }
+
+ private updateWhitelistPaths(installed: InstallationDetails[], running: any[]) {
+ const paths = new Set();
+ installed.forEach(miner => {
+ if (miner.miner_binary) paths.add(miner.miner_binary);
+ if (miner.config_path) paths.add(miner.config_path);
+ });
+ running.forEach(miner => {
+ if (miner.configPath) paths.add(miner.configPath);
+ });
+ this.whitelistPaths = Array.from(paths);
+ }
+
+ installMiner(minerType: string): void {
+ this.actionInProgress = `install-${minerType}`;
+ this.error = null;
+ this.http.post(`${this.apiBaseUrl}/miners/${minerType}/install`, {}).subscribe({
+ next: () => {
+ setTimeout(() => {
+ this.getAdminData();
+ // A simple way to signal completion is to reload the page
+ // so the main dashboard component re-evaluates its state.
+ if (this.needsSetup) {
+ window.location.reload();
+ }
+ }, 1000);
+ },
+ error: (err: HttpErrorResponse) => this.handleError(err, `Failed to install ${minerType}`)
+ });
+ }
+
+ uninstallMiner(minerType: string): void {
+ this.actionInProgress = `uninstall-${minerType}`;
+ this.error = null;
+ this.http.delete(`${this.apiBaseUrl}/miners/${minerType}/uninstall`).subscribe({
+ next: () => {
+ setTimeout(() => {
+ this.getAdminData();
+ }, 1000);
+ },
+ error: (err: HttpErrorResponse) => this.handleError(err, `Failed to uninstall ${minerType}`)
+ });
+ }
+
+ getMinerType(miner: any): string {
+ if (!miner.path) return 'unknown';
+ const parts = miner.path.split('/').filter((p: string) => p);
+ return parts.length > 1 ? parts[parts.length - 2] : parts[parts.length - 1] || 'unknown';
+ }
+}
diff --git a/ui/src/app/app.css b/ui/src/app/app.css
index a00e46e..97043a3 100644
--- a/ui/src/app/app.css
+++ b/ui/src/app/app.css
@@ -42,6 +42,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
+ margin-top: 1rem;
}
.miner-item-container {
@@ -79,36 +80,8 @@
padding-top: 1rem;
}
-.admin-panel {
- padding: 1rem;
- border-top: 1px solid #e0e0e0;
- margin-top: 1rem;
- border-radius: 0.25rem;
- border: 1px solid #e0e0e0;
-}
-
-.admin-title {
- margin-top: 0;
- border-bottom: 1px solid #e0e0e0;
- padding-bottom: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.path-list ul {
- list-style: none;
- padding: 0.5rem;
- margin: 0;
- font-family: monospace;
- border-radius: 0.25rem;
- border: 1px solid #e0e0e0;
-}
-
-.path-list li {
- padding: 0.25rem 0;
-}
-
.button-spinner {
- font-size: 1em; /* Make spinner same size as button text */
+ font-size: 1em;
margin: 0;
}
diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts
index fcac68b..05edeab 100644
--- a/ui/src/app/app.ts
+++ b/ui/src/app/app.ts
@@ -36,7 +36,7 @@ interface AvailableMiner {
}
@Component({
- selector: 'mde-mining-dashboard',
+ selector: 'snider-mining-dashboard',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule, HttpClientModule, FormsModule],
@@ -52,7 +52,7 @@ export class MiningDashboardElementComponent implements OnInit {
apiAvailable: boolean = true;
error: string | null = null;
showAdminPanel: boolean = false;
- actionInProgress: string | null = null; // To track which miner action is running
+ actionInProgress: string | null = null;
systemInfo: any = null;
manageableMiners: any[] = [];
@@ -73,6 +73,7 @@ export class MiningDashboardElementComponent implements OnInit {
private handleError(err: HttpErrorResponse, defaultMessage: string) {
console.error(err);
+ this.actionInProgress = null;
if (err.error && err.error.error) {
this.error = `${defaultMessage}: ${err.error.error}`;
} else if (typeof err.error === 'string' && err.error.length < 200) {
diff --git a/ui/src/index.html b/ui/src/index.html
index 2ef73fc..d8ff6c8 100644
--- a/ui/src/index.html
+++ b/ui/src/index.html
@@ -8,6 +8,7 @@
-
+
+