From 27834c332cbbcadccfa2966bca375a284f88eb3b Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 10 Nov 2025 02:22:37 +0000 Subject: [PATCH] integrates Highcharts for live hashrate visualization and updates dashboard layout --- ui/angular.json | 3 +- ui/package-lock.json | 23 +++++ ui/package.json | 2 + ui/src/app/app.config.ts | 28 +++++- ui/src/app/app.css | 79 ++++++++++++++++ ui/src/app/app.html | 51 ++++++++++ ui/src/app/app.ts | 197 ++++++++++++--------------------------- ui/src/index.html | 2 +- ui/src/main.ts | 10 +- ui/src/styles.css | 2 + 10 files changed, 249 insertions(+), 148 deletions(-) diff --git a/ui/angular.json b/ui/angular.json index a5cb63e..2378f74 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -27,8 +27,7 @@ } ], "styles": [ - "src/styles.css", - "node_modules/@awesome.me/webawesome/dist/styles/themes/awesome.css" + "src/styles.css" ], "scripts": [], "outputHashing": "none", diff --git a/ui/package-lock.json b/ui/package-lock.json index e0d1153..0ef2ae2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -17,6 +17,8 @@ "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", "@awesome.me/webawesome": "^3.0.0", + "highcharts": "^12.4.0", + "highcharts-angular": "^5.2.0", "install": "^0.13.0", "npm": "^11.6.2", "rxjs": "~7.8.0", @@ -8099,6 +8101,27 @@ "node": ">= 0.4" } }, + "node_modules/highcharts": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.4.0.tgz", + "integrity": "sha512-o6UxxfChSUrvrZUbWrAuqL1HO/+exhAUPcZY6nnqLsadZQlnP16d082sg7DnXKZCk1gtfkyfkp6g3qkIZ9miZg==", + "license": "https://www.highcharts.com/license", + "peer": true + }, + "node_modules/highcharts-angular": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/highcharts-angular/-/highcharts-angular-5.2.0.tgz", + "integrity": "sha512-ba7Q0d3F0SDNWqGSzbe+Khi2FF7wt0kY3hwocAWpaaKSKF3EHajUqOSqs9LY+nXXOOz8SCRtSbgaSf0cLdx1xA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "highcharts": ">=12.2.0" + } + }, "node_modules/hosted-git-info": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 2640ca5..a6e3bd8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,6 +31,8 @@ "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", "@awesome.me/webawesome": "^3.0.0", + "highcharts": "^12.4.0", + "highcharts-angular": "^5.2.0", "install": "^0.13.0", "npm": "^11.6.2", "rxjs": "~7.8.0", diff --git a/ui/src/app/app.config.ts b/ui/src/app/app.config.ts index d953f4c..4f117cd 100644 --- a/ui/src/app/app.config.ts +++ b/ui/src/app/app.config.ts @@ -1,5 +1,6 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideHighcharts } from 'highcharts-angular'; import { routes } from './app.routes'; @@ -7,6 +8,31 @@ export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes) + provideRouter(routes), + provideHighcharts({ + // Optional: Define the Highcharts instance dynamically + instance: () => import('highcharts'), + + // Global chart options applied across all charts + options: { + title: { + style: { + color: 'tomato', + }, + }, + legend: { + enabled: false, + }, + }, + + // Include Highcharts additional modules (e.g., exporting, accessibility) or custom themes + modules: () => { + return [ + import('highcharts/esm/modules/accessibility'), + import('highcharts/esm/modules/exporting'), + import('highcharts/esm/themes/sunset'), + ]; + }, + }), ] }; diff --git a/ui/src/app/app.css b/ui/src/app/app.css index e69de29..67b3b9b 100644 --- a/ui/src/app/app.css +++ b/ui/src/app/app.css @@ -0,0 +1,79 @@ +:host { + display: block; + font-family: sans-serif; +} + +.card { + max-width: 800px; + margin: 20px auto; +} + +.header-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.header-title { + margin: 0; + font-size: 1.25rem; + flex-grow: 1; +} + +.title-quiet { + color: var(--wa-color-text-quiet); +} + +.header-stats { + display: flex; + gap: 1rem; + align-items: center; +} + +.stat-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; +} + +.chart { + width: 100%; + height: 400px; + display: block; +} + +.details-section { + padding: 1rem; + background-color: var(--wa-color-neutral-50); + border-radius: var(--wa-border-radius-medium); +} + +.history-list { + list-style: none; + padding: 0; +} + +.card-footer { + display: flex; + justify-content: flex-end; +} + +.error-message { + color: var(--wa-color-danger-600); +} + +/* Responsive stacking for smaller screens */ +@media (max-width: 600px) { + .header-container { + flex-direction: column; + align-items: flex-start; + } + + .header-stats { + width: 100%; + justify-content: space-between; + } +} diff --git a/ui/src/app/app.html b/ui/src/app/app.html index e69de29..8bb5afa 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -0,0 +1,51 @@ + + +
+

+ Mining Dashboard: {{ minerName }} +

+
+
+ + {{ currentHashrate.toFixed(2) }} H/s +
+
+ + {{ lastUpdated | date:'mediumTime' }} +
+
+ + + +
+ + + + + +
diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 82b8b9f..06b32de 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -11,12 +11,15 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; import { CommonModule } from '@angular/common'; import { interval, Subscription } from 'rxjs'; import { switchMap, startWith } from 'rxjs/operators'; -import "@awesome.me/webawesome/dist/webawesome.js"; +import * as Highcharts from 'highcharts'; +import { HighchartsChartComponent } from 'highcharts-angular'; + // Import Shoelace components +import "@awesome.me/webawesome/dist/webawesome.js"; import '@awesome.me/webawesome/dist/components/card/card.js'; import '@awesome.me/webawesome/dist/components/button/button.js'; +import '@awesome.me/webawesome/dist/components/tooltip/tooltip.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; -// Add other Shoelace components as needed for your UI interface HashratePoint { timestamp: string; // ISO string @@ -24,118 +27,17 @@ interface HashratePoint { } @Component({ - selector: 'mde-mining-dashboard', // This will be your custom element tag + selector: 'mde-mining-dashboard', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [CommonModule, HttpClientModule], // HttpClientModule is needed for HttpClient - template: ` - -
- -

Mining Dashboard: {{ minerName }}

-
- -

Loading hashrate data...

-

Error: {{ error }}

- -
-

Current Hashrate: {{ currentHashrate }} H/s

-

Last Updated: {{ lastUpdated | date:'mediumTime' }}

- -

Hashrate History ({{ hashrateHistory.length }} points)

-
    -
  • - {{ point.timestamp | date:'mediumTime' }}: {{ point.hashrate }} H/s -
  • -
  • ...
  • -
- - -
- - - -
-
Raw History Data:
-
{{ hashrateHistory | json }}
-
-
- `, - styles: [` - .mining-dashboard-card { - width: 100%; - max-width: 500px; - margin: 20px auto; - border: 1px solid var(--wa-color-neutral-300); - border-radius: var(--wa-border-radius-medium); - box-shadow: var(--wa-shadow-medium); - } - .card-header { - display: flex; - align-items: center; - gap: 10px; - padding: var(--wa-spacing-medium); - border-bottom: 1px solid var(--wa-color-neutral-200); - } - .card-header h3 { - margin: 0; - font-size: var(--wa-font-size-large); - } - .card-header sl-icon { - font-size: var(--wa-font-size-x-large); - color: var(--wa-color-primary-500); - } - .error-message { - color: var(--wa-color-danger-500); - font-weight: bold; - } - .history-list { - list-style-type: none; - padding: 0; - max-height: 150px; - overflow-y: auto; - border: 1px solid var(--wa-color-neutral-100); - border-radius: var(--wa-border-radius-small); - padding: var(--wa-spacing-x-small); - background-color: var(--wa-color-neutral-50); - } - .history-list li { - padding: var(--wa-spacing-2x-small) 0; - border-bottom: 1px dotted var(--wa-color-neutral-100); - } - .history-list li:last-child { - border-bottom: none; - } - .card-footer { - display: flex; - justify-content: flex-end; - gap: var(--wa-spacing-small); - padding-top: var(--wa-spacing-medium); - border-top: 1px solid var(--wa-color-neutral-200); - } - .details-section { - margin-top: var(--wa-spacing-medium); - padding: var(--wa-spacing-small); - background-color: var(--wa-color-neutral-50); - border: 1px solid var(--wa-color-neutral-200); - border-radius: var(--wa-border-radius-small); - } - .details-section pre { - white-space: pre-wrap; - word-break: break-all; - font-size: var(--wa-font-size-x-small); - max-height: 200px; - overflow-y: auto; - } - `], - encapsulation: ViewEncapsulation.ShadowDom // Crucial for isolation + imports: [CommonModule, HttpClientModule, HighchartsChartComponent], + templateUrl: './app.html', + styleUrls: ["app.css"], + encapsulation: ViewEncapsulation.ShadowDom }) export class MiningDashboardElementComponent implements OnInit, OnDestroy { - @Input() minerName: string = 'xmrig'; // Default miner name - @Input() apiBaseUrl: string = 'http://localhost:9090/api/v1/mining'; // Default API base URL + @Input() minerName: string = 'xmrig'; + @Input() apiBaseUrl: string = 'http://localhost:9090/api/v1/mining'; hashrateHistory: HashratePoint[] = []; currentHashrate: number = 0; @@ -146,6 +48,36 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy { private refreshSubscription: Subscription | undefined; + chartOptions: Highcharts.Options = { + chart: { + type: 'spline', + }, + title: { + text: 'Live Hashrate' + }, + xAxis: { + type: 'datetime', + title: { + text: 'Time' + } + }, + yAxis: { + title: { + text: 'Hashrate (H/s)' + }, + min: 0 + }, + series: [{ + name: 'Hashrate', + type: 'line', + data: [] + }], + credits: { + enabled: false + } + }; + updateFlag = false; + constructor(private http: HttpClient, private elementRef: ElementRef) {} ngOnInit(): void { @@ -157,18 +89,34 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy { } startAutoRefresh(): void { - this.stopAutoRefresh(); // Stop any existing refresh - this.refreshSubscription = interval(10000) // Refresh every 10 seconds + this.stopAutoRefresh(); + this.refreshSubscription = interval(10000) .pipe(startWith(0), switchMap(() => this.fetchHashrateObservable())) .subscribe({ next: (history) => { this.hashrateHistory = history; - if (history.length > 0) { + if (history && history.length > 0) { this.currentHashrate = history[history.length - 1].hashrate; this.lastUpdated = new Date(history[history.length - 1].timestamp); + + const chartData = history.map(point => [ + new Date(point.timestamp).getTime(), + point.hashrate + ]); + + // Safely update the chart data with type assertion + if (this.chartOptions.series && this.chartOptions.series[0]) { + (this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = chartData; + this.updateFlag = true; // Trigger chart update + } } else { this.currentHashrate = 0; this.lastUpdated = null; + // Safely clear the chart data + if (this.chartOptions.series && this.chartOptions.series[0]) { + (this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = []; + this.updateFlag = true; + } } this.loading = false; this.error = null; @@ -188,29 +136,6 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy { } } - fetchHashrate(): void { - this.loading = true; - this.error = null; - this.fetchHashrateObservable().subscribe({ - next: (history) => { - this.hashrateHistory = history; - if (history.length > 0) { - this.currentHashrate = history[history.length - 1].hashrate; - this.lastUpdated = new Date(history[history.length - 1].timestamp); - } else { - this.currentHashrate = 0; - this.lastUpdated = null; - } - this.loading = false; - }, - error: (err) => { - console.error('Failed to fetch hashrate history:', err); - this.error = 'Failed to fetch hashrate history.'; - this.loading = false; - } - }); - } - private fetchHashrateObservable() { const url = `${this.apiBaseUrl}/miners/${this.minerName}/hashrate-history`; return this.http.get(url); diff --git a/ui/src/index.html b/ui/src/index.html index 07d57d2..2ef73fc 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -1,5 +1,5 @@ - + Ui diff --git a/ui/src/main.ts b/ui/src/main.ts index 283f450..cac705b 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -1,18 +1,12 @@ import { createApplication } from '@angular/platform-browser'; import { createCustomElement } from '@angular/elements'; import { MiningDashboardElementComponent } from './app/app'; // Renamed App to MiningDashboardElementComponent -import { importProvidersFrom } from '@angular/core'; -import { HttpClientModule } from '@angular/common/http'; -import { CommonModule } from '@angular/common'; +import {appConfig} from './app/app.config'; (async () => { // Bootstrap a minimal Angular application to provide // necessary services like HttpClient to the custom element. - const app = await createApplication({ - providers: [ - importProvidersFrom(HttpClientModule, CommonModule) - ] - }); + const app = await createApplication(appConfig); // Define your custom element const MiningDashboardElement = createCustomElement(MiningDashboardElementComponent, { injector: app.injector }); diff --git a/ui/src/styles.css b/ui/src/styles.css index 90d4ee0..0101300 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -1 +1,3 @@ /* You can add global styles to this file, and also import other style files */ +@import "@awesome.me/webawesome/dist/styles/webawesome.css"; +@import "@awesome.me/webawesome/dist/styles/themes/awesome.css";